- Product mix: linear program and its graph
- Availability of a machine with a Markov chain
- Newsvendor: optimal order under normal demand
- Call centre staffing with Erlang C
- PERT: probability to meet the contract deadline
- EOQ, safety stock and reorder point
- Trend and Q4 peak forecast by QR least squares
- Weibull bearing: age replacement policy
- Leontief input-output model of three sectors
- Discrete newsvendor from an empirical demand
Product mix: linear program and its graph
Maximise the profit 40x + 30y of two products under machine, labour and market constraints, find the optimal vertex as the intersection of the two saturated constraints, and draw the feasible region with the iso-profit line.
#1. Product mix: profit 40x + 30y, machine 2x + y <= 100, labour x + y <= 80, market x <= 40; -> "[opt]" (num maximize "40*x+30*y" "{\"x\": 1, \"y\": 1}" "{\"constraints\": [\"2*x + y <= 100\", \"x + y <= 80\", \"x <= 40\"], \"bounds\": {\"x\": [0, null], \"y\": [0, null]}}"); -> "[z]" (json load select "rx_lp" [opt] /value); #2. The optimum is the vertex where machine and labour are both saturated; -> "[v]" (num solve_system "[\"2*x + y = 100\", \"x + y = 80\"]" "{\"x\": 10, \"y\": 10}"); json load "rx_vx" [v]; #3. Draw the constraints, the market limit, the iso-profit line and the optimal vertex; plot function (concat "[\"100-2*x\", \"80-x\", \"(" [z] "-40*x)/30\"]") "x" 0 40; plot add (plot xy "[40, 40]" "[0, 80]"); plot add (plot points (concat "[" (json select "rx_vx" /solution/x) "]") (concat "[" (json select "rx_vx" /solution/y) "]"))
The chart is shown in the Plot tab of the editor.
- Read the shadow prices: increase the machine capacity by 1 and run
num maximizeagain - Solve the vertex exactly with
cas solve
Availability of a machine with a Markov chain
A machine is up, degraded or down (then repaired) from one day to the next. Compute the stationary law, check it with P^50, then the availability, the mean daily margin and the mean number of days before a breakdown (fundamental matrix).
#1. Daily transitions of a machine: up, degraded, down - repaired; -> "[P]" "[[0.90, 0.08, 0.02], [0.00, 0.85, 0.15], [0.60, 0.00, 0.40]]"; #2. Stationary law: pi P = pi and sum pi = 1, system Pt - I with the last row replaced by ones; -> "[M]" (matrix sub (matrix transpose [P]) (matrix identity 3)); -> "[M]" (matrix set (matrix set (matrix set [M] 3 1 1) 3 2 1) 3 3 1); -> "[pi]" (matrix solve [M] "[0, 0, 1]"); #3. Check: any row of P^50 converges to pi; -> "[P50]" (matrix row (matrix pow [P] 50) 1); #4. Mean days before a breakdown: fundamental matrix N = I - Q inverse on the up and degraded states; -> "[Q]" "[[0.90, 0.08], [0.00, 0.85]]"; -> "[N]" (matrix inverse (matrix sub (matrix identity 2) [Q])); -> "[T]" (matrix mul [N] "[1, 1]"); concat "stationary " [pi] ", P^50 row 1 " [P50] ", availability " (math round (math calc (concat "1-" (matrix get (concat "[" [pi] "]") 1 3))) 4) ", margin per day " (math round (vector dot [pi] "[1000, 600, -400]") 2) " EUR, days to breakdown from up / degraded " [T]
stationary [0.5882352941176472,0.31372549019607837,0.0980392156862745], P^50 row 1 [0.5882354296305502,0.313725390547185,0.09803917982226582], availability 0.902, margin per day 737.25 EUR, days to breakdown from up / degraded [15.333333333333336,6.666666666666666]
- Compare two maintenance policies by changing the second row of P
- Look at the speed of convergence with the second eigenvalue of
matrix eigen
Newsvendor: optimal order under normal demand
A seasonal product sells 12, costs 5 and is salvaged at 1, with a normal demand (500, 120). Find the order quantity from the critical ratio with erf, confirm it by minimising the expected overage and underage cost, and compute the expected leftover and profit.
#1. Newsvendor: demand normal mean 500 sd 120, price 12, cost 5, salvage 1; -> "[mu]" 500; -> "[s]" 120; -> "[p]" 12; -> "[c]" 5; -> "[v]" 1; #2. Critical ratio CR = cu / cu + co, and q* with Phi of z = CR, Phi written with erf; -> "[cr]" (math calc "([p]-[c])/([p]-[v])"); -> "[q]" (num solve (concat "0.5*(1+erf((q-" [mu] ")/(" [s] "*sqrt(2))))-" [cr]) "q" 0 1000); #3. Expected cost of overage and underage with the normal loss function; -> "[z]" (concat "((q-" [mu] ")/" [s] ")"); -> "[L]" (concat "(exp(-" [z] "^2/2)/sqrt(2*pi)-" [z] "*0.5*erfc(" [z] "/sqrt(2)))"); -> "[cost]" (concat "(" [c] "-" [v] ")*(q-" [mu] "+" [s] "*" [L] ")+(" [p] "-" [c] ")*" [s] "*" [L]); -> "[best]" (num minimize [cost] "q" 0 1000); #4. Checks: derivative zero at q*, expected leftover by integration; -> "[slope]" (num derivative [cost] "q" [q]); -> "[left]" (num integrate (concat "(" [q] "-x)*exp(-((x-" [mu] ")/" [s] ")^2/2)/(" [s] "*sqrt(2*pi))") "x" "-inf" [q]); concat "CR = " (math round [cr] 4) ", q* = " (math round [q] 1) ", minimize: " [best] ", slope at q* = " (math round [slope] 8) ", expected leftover " (math round [left] 1) " units, expected profit " (math round (math calc (concat "([p]-[c])*" [mu] "-" (json load select "rx_nv" [best] /value))) 2)
CR = 0.6364, q* = 541.9, minimize: {"x": 541.8506834620454, "value": 495.5326352140132}, slope at q* = 0, expected leftover 71.7 units, expected profit 3004.47- Draw the expected cost with
plot functionand the cost formula - Simulate 1000 seasons with
stat dist_random"normal" and compare
Call centre staffing with Erlang C
A call centre receives 120 calls per hour with an average handling time of 4 minutes. The script increases the number of agents until the 80/20 service level is met, with Erlang B taken from the Poisson law and Erlang C, then gives the average speed of answer and the occupancy.
#1. Call centre: 120 calls per hour, average handling time 240 s, offered traffic A in Erlangs; -> "[lam]" 120; -> "[aht]" 240; -> "[A]" (math calc "[lam]*[aht]/3600"); #2. Target 80/20: 80 % of the calls answered within 20 s. Smallest number of agents c; -> "[c]" (math ceiling [A]); -> "[sl]" 0; while (< [sl] 0.8) { ++ "[c]"; #Erlang B from the Poisson law, then Erlang C; -> "[B]" (/ (stat dist_pdf "poisson" (concat "[" [A] "]") [c]) (stat dist_cdf "poisson" (concat "[" [A] "]") [c])); -> "[C]" (math calc "[c]*[B]/([c]-[A]*(1-[B]))"); -> "[sl]" (math calc "1-[C]*exp(-([c]-[A])*20/[aht])"); }; #3. Average speed of answer and occupancy; -> "[asa]" (math calc "[C]*[aht]/([c]-[A])"); concat "A = " [A] " Erlangs, agents = " [c] ", P(wait) = " (math round [C] 4) ", service level = " (math round (* 100 [sl]) 1) " % in 20 s, ASA = " (math round [asa] 1) " s, occupancy = " (math round (math calc "100*[A]/[c]") 1) " %"
A = 8 Erlangs, agents = 11, P(wait) = 0.245, service level = 80.9 % in 20 s, ASA = 19.6 s, occupancy = 72.7 %
- Add a shrinkage of 30 % for breaks and training: scheduled agents = c / 0.7 with
math ceiling - Check the M/M/c result with a discrete-event simulation drawing times with
stat dist_random
PERT: probability to meet the contract deadline
The four tasks of a critical path have optimistic, most likely and pessimistic durations. Beta-PERT gives each expected duration as a 1-4-1 weighted mean and its variance; the normal approximation gives the probability to finish within 40 days and the 95 % date.
#1. PERT: optimistic, most likely, pessimistic durations in days of the critical path tasks; -> "[w]" "[1, 4, 1]"; -> "[d1]" (stat weighted_mean "[4, 6, 10]" [w]); -> "[d2]" (stat weighted_mean "[8, 12, 22]" [w]); -> "[d3]" (stat weighted_mean "[3, 5, 9]" [w]); -> "[d4]" (stat weighted_mean "[6, 9, 18]" [w]); #2. Beta-PERT variances sigma^2 = b-a over 6 squared, summed along the path; -> "[var]" (stat sum_sq (concat "[" (math calc "(10-4)/6") ", " (math calc "(22-8)/6") ", " (math calc "(9-3)/6") ", " (math calc "(18-6)/6") "]")); -> "[T]" (math calc "[d1]+[d2]+[d3]+[d4]"); #3. Probability to finish within the 40-day contract, and the date with 95 % confidence; -> "[law]" (concat "[" [T] ", " (math sqrt [var]) "]"); -> "[p40]" (stat dist_cdf "normal" [law] 40); -> "[d95]" (stat dist_inv "normal" [law] 0.95); concat "durations " (math round [d1] 2) ", " (math round [d2] 2) ", " (math round [d3] 2) ", " (math round [d4] 2) " d; critical path = " (math round [T] 2) " d, sigma = " (math round (math sqrt [var]) 2) " d; P(T <= 40 d) = " (math round (* 100 [p40]) 1) " %; 95 % date = " (math round [d95] 1) " d"
durations 6.33, 13, 5.33, 10 d; critical path = 34.67 d, sigma = 3.38 d; P(T <= 40 d) = 94.3 %; 95 % date = 40.2 d
- Check the near-critical paths too: a parallel path with a larger variance can be the real risk
- Simulate triangular durations with
stat dist_randomand compare the histogram withplot histogram
EOQ, safety stock and reorder point
A spare part sells 12000 units a year. The script computes the economic order quantity, then the safety stock and reorder point for a 97.5 % cycle service level, getting z from the inverse error function and checking it with erf.
#1. Spare part: 12000 units a year, 150 EUR per order, holding cost 2.40 EUR per unit and year; -> "[D]" 12000; -> "[S]" 150; -> "[H]" 2.4; #2. Economic order quantity Q* = sqrt 2DS/H and its yearly cost; -> "[Q]" (math calc "sqrt(2*[D]*[S]/[H])"); -> "[TC]" (math calc "[D]/[Q]*[S]+[Q]/2*[H]"); #3. Service level 97.5 % on a 2-week lead time, weekly demand sigma 40: z from the inverse error function; -> "[z]" (math calc (concat "sqrt(2)*" (math erf_inv (math calc "2*0.975-1")))); -> "[SS]" (math calc "[z]*40*sqrt(2)"); -> "[ROP]" (math calc "[D]/52*2+[SS]"); #4. Check: probability of no stock-out during the lead time, Phi z = 1/2 1 + erf z/sqrt 2; -> "[p]" (math calc (concat "0.5*(1+" (math erf (math calc "[z]/sqrt(2)")) ")")); concat "Q* = " (math rint [Q]) " units (" (math round (math calc "[D]/[Q]") 1) " orders a year, cost " (math round [TC] 2) " EUR); z = " (math round [z] 4) ", safety stock " (math ceil [SS]) ", reorder point " (math ceil [ROP]) " units; cycle service level check " (math round [p] 4)
Q* = 1225 units (9.8 orders a year, cost 2939.39 EUR); z = 1.96, safety stock 111, reorder point 573 units; cycle service level check 0.975
- Add quantity discounts and compare the total cost of each price break
- Use the fill rate with the loss function instead of the cycle service level
Trend and Q4 peak forecast by QR least squares
Eight quarters of demand show a trend and a Q4 peak. The script fits y = a + b t + c Q4 with a QR decomposition (no normal equations), reads the residual sum of squares from Q^T y and forecasts the next Q4.
#1. Quarterly demand of a product over 2 years, model y = a + b t + c Q4 with a Q4 peak dummy; -> "[y]" "[410, 432, 445, 560, 470, 488, 502, 618]"; -> "[X]" "[[1, 1, 0], [1, 2, 0], [1, 3, 0], [1, 4, 1], [1, 5, 0], [1, 6, 0], [1, 7, 0], [1, 8, 1]]"; #2. QR decomposition of the design matrix, stable least squares without the normal equations; json load "rx_p4b_qr" (matrix qr [X]); -> "[Qty]" (matrix mul (matrix transpose (json select "rx_p4b_qr" /Q)) [y]); -> "[R]" (concat "[" (matrix row (json select "rx_p4b_qr" /R) 1) "," (matrix row (json select "rx_p4b_qr" /R) 2) "," (matrix row (json select "rx_p4b_qr" /R) 3) "]"); #3. Back substitution R1 beta = first 3 terms of Q^T y, the other terms give the residual sum of squares; -> "[beta]" (matrix solve [R] (concat "[" (atom get (string replace (string replace [Qty] "[" "") "]" "") 1 ",") "," (atom get (string replace (string replace [Qty] "[" "") "]" "") 2 ",") "," (atom get (string replace (string replace [Qty] "[" "") "]" "") 3 ",") "]")); -> "[rss]" (math calc (concat (vector dot [Qty] [Qty]) "-" (vector dot (matrix mul [R] [beta]) (matrix mul [R] [beta])))); json unload "rx_p4b_qr"; #4. Forecast of the next Q4, t = 12; concat "a, b, c = " [beta] "; RSS = " (math round [rss] 2) "; forecast Q4 of year 3: " (math round (vector dot [beta] "[1, 12, 1]") 0) " units"
a, b, c = [399.0555555555555,14.694444444444438,101.77777777777776]; RSS = 37.47; forecast Q4 of year 3: 677 units
- Add one dummy per quarter and compare the RSS
- Check the coefficients with
matrix solveon X directly, which also uses QR
Weibull bearing: age replacement policy
A pump bearing follows a Weibull law (beta 2.5, eta 1200 h). The script gives the MTTF with the Gamma function and scans the preventive replacement age that minimises the cost per hour against run-to-failure.
#1. Pump bearing, Weibull life beta = 2.5, eta = 1200 h, preventive change 400 EUR, failure 3000 EUR; -> "[b]" 2.5; -> "[eta]" 1200; #2. Mean time to failure eta Gamma 1+1/beta; -> "[mttf]" (math calc (concat [eta] "*" (math gamma (math calc "1+1/[b]")))); #3. Age replacement: cost per hour C T = Cp R T + Cf F T over the integral of R from 0 to T; -> "[best]" ""; -> "[cmin]" 1000000; for (-> "[T]" 200) (<= [T] 1400) (-> "[T]" (+ [T] 50)) { -> "[den]" (num integrate "exp(-(t/1200)^2.5)" "t" 0 [T]); -> "[cT]" (math calc (concat "(400*exp(-(" [T] "/1200)^2.5)+3000*(1-exp(-(" [T] "/1200)^2.5)))/" [den])); if (< [cT] [cmin]) { -> "[cmin]" [cT]; -> "[best]" [T]; }; }; #4. Compare with run-to-failure, cost 3000 / MTTF; concat "MTTF = " (math round [mttf] 1) " h; best preventive interval " [best] " h at " (math round [cmin] 4) " EUR/h, against " (math round (math calc "3000/[mttf]") 4) " EUR/h if run to failure: saving " (math round (math calc "(1-[cmin]*[mttf]/3000)*100") 1) " %"
MTTF = 1064.7 h; best preventive interval 500 h at 1.3945 EUR/h, against 2.8177 EUR/h if run to failure: saving 50.5 %
- Refine the optimum with
num minimizearound the best grid point - Add the downtime cost and an inspection interval
Leontief input-output model of three sectors
With the technical coefficients of agriculture, industry and services, the script builds the Leontief matrix I - A, reads its determinant from the LU factors, solves the total output for the final demand and gives the output multipliers.
#1. Input-output table of 3 sectors: agriculture, industry, services, technical coefficients A; -> "[A]" "[[0.15, 0.10, 0.05], [0.25, 0.35, 0.15], [0.10, 0.20, 0.25]]"; -> "[d]" "[120, 300, 450]"; #2. Leontief matrix I - A and its LU factors; -> "[M]" (matrix sub (matrix identity 3) [A]); json load "rx_p4b_lu" (matrix lu [M]); -> "[U]" (json select "rx_p4b_lu" /U); -> "[det]" (math calc (concat (matrix get [U] 1 1) "*" (matrix get [U] 2 2) "*" (matrix get [U] 3 3))); json unload "rx_p4b_lu"; #3. Total output needed for the final demand: I - A x = d; -> "[x]" (matrix solve [M] [d]); #4. Output multipliers: column sums of the Leontief inverse; -> "[Li]" (matrix inverse [M]); -> "[mult]" (matrix row (matrix mul "[[1, 1, 1]]" [Li]) 1); concat "det(I-A) = " (math round [det] 5) " from the U diagonal, matrix det gives " (math round (matrix det [M]) 5) "; output x = " [x] "; multipliers " [mult]
det(I-A) = 0.36287 from the U diagonal, matrix det gives 0.36287; output x = [280.4684808818464,763.4860489149157,840.9920771615571]; multipliers [2.1357216672407855,2.4733034791594903,1.9703754736479504]
- Check the Hawkins-Simon condition: all leading minors of I - A positive
- Simulate a 10 % rise of the services demand
Discrete newsvendor from an empirical demand
A bakery knows the daily sales of a fresh cake over 20 days. The script builds the frequency table, the critical ratio Cu/(Cu + Co) and picks the smallest quantity whose cumulative frequency reaches it.
#1. Daily sales of a fresh cake over 20 days: cost 4 EUR, price 10 EUR, unsold sold off at 1 EUR; -> "[sales]" "[18, 22, 20, 25, 20, 19, 23, 22, 20, 26, 21, 18, 22, 24, 20, 22, 19, 23, 21, 22]"; #2. Empirical distribution of the demand; json load "rx_p4b_freq" (stat frequency [sales]); #3. Critical ratio Cu / Cu + Co: under-stock costs 10-4, over-stock costs 4-1; -> "[cr]" (math calc "(10-4)/((10-4)+(4-1))"); #4. Order the smallest quantity whose cumulative frequency reaches the critical ratio; -> "[q]" ""; -> "[i]" 0; while (is empty [q]) { if (>= (json select "rx_p4b_freq" (concat "/[" [i] "]/cumulative_frequency")) [cr]) { -> "[q]" (json select "rx_p4b_freq" (concat "/[" [i] "]/value")); }; ++ "[i]"; }; -> "[nv]" (json count "rx_p4b_freq" /); json unload "rx_p4b_freq"; concat [nv] " distinct demand values, most frequent " (stat mode [sales]) ", mean " (stat mean [sales]) "; critical ratio " (math round [cr] 4) ": bake " [q] " cakes a day"
9 distinct demand values, most frequent [22], mean 21.35; critical ratio 0.6667: bake 22 cakes a day
- Compute the expected profit of each quantity from the table
- Compare with the normal approximation
stat dist_inv
