Skip to main content

software_engineering/
lean_value_stream_metrics.rs

1//! # Lean Value Stream Metrics
2//!
3//! Every flow metric in this crate descends from the five baseline
4//! measurements of classical Lean value stream mapping, developed at Toyota
5//! and generalized across manufacturing, operations, and service delivery
6//! long before software adopted them: lead time, process time, cycle time,
7//! percent complete and accurate (%C/A), and takt time. This module
8//! implements %C/A, rolled throughput yield, and takt time — the three that
9//! do not already have a direct home elsewhere in this crate.
10//!
11//! ## Formula
12//!
13//! ```text
14//! %C/A = usable units without rework / total units × 100%
15//! Rolled throughput yield = %C/A(stage 1) × %C/A(stage 2) × ... × %C/A(stage N)
16//! Takt time = available working time / customer demand over that period
17//! ```
18//!
19//! ## Why it matters
20//!
21//! %C/A measures something the flow metrics do not: how much of what a
22//! stage produces is actually usable by the next stage without being sent
23//! back. Rolled up across a multi-stage value stream (rolled throughput
24//! yield), it reveals how rework compounds invisibly across handoffs: three
25//! stages each individually running at 90% complete-and-accurate compound to
26//! roughly 73% overall — a number that looks nothing like any single
27//! stage's own report and is usually the more honest one. Takt time
28//! reframes capacity planning around real customer demand rather than
29//! existing pace.
30//!
31//! ## Example
32//!
33//! ```rust
34//! use software_engineering::lean_value_stream_metrics::{
35//!     percent_complete_and_accurate, rolled_throughput_yield, takt_time,
36//! };
37//!
38//! // A stage that produces 90 usable units out of 100: 90% C/A.
39//! let stage_pca = percent_complete_and_accurate(90.0, 100.0).unwrap();
40//! assert!((stage_pca - 90.0).abs() < 1e-9);
41//!
42//! // Three stages each at 90% C/A compound to about 73% rolled throughput yield.
43//! let rty = rolled_throughput_yield(&[0.90, 0.90, 0.90]);
44//! assert!((rty - 0.729).abs() < 1e-9);
45//!
46//! // 400 minutes of available working time against demand for 20 units: 20 min/unit.
47//! let takt = takt_time(400.0, 20.0).unwrap();
48//! assert_eq!(takt, 20.0);
49//! ```
50//!
51//! ## Pitfalls
52//!
53//! - **Measuring %C/A only at final delivery**, the chapter's central
54//!   gaming vector: a team can report a high final-stage %C/A while earlier
55//!   stages quietly produce rework fixed before anyone measures it. Roll
56//!   %C/A up multiplicatively across every stage instead.
57//! - **Setting takt time from current capacity instead of real customer
58//!   demand** defeats the purpose of the metric, which is to reveal a gap
59//!   between demand and capacity.
60//! - **Reporting %C/A without pairing it against flow velocity** allows a
61//!   rising throughput number to hide a falling rework rate.
62//!
63//! ## Sources
64//!
65//! - Rother, Mike, and John Shook. *Learning to See: Value Stream Mapping to
66//!   Create Value and Eliminate Muda*. Lean Enterprise Institute, 1999.
67//! - Ohno, Taiichi. *Toyota Production System: Beyond Large-Scale
68//!   Production*. Productivity Press, 1988.
69//!
70//! Topic doc: 02-08-lean-value-stream-metrics.md
71
72/// Percent complete and accurate (%C/A): the share of a stage's output that
73/// a downstream team can use without rework.
74///
75/// Measure %C/A at each stage individually so it can be rolled up
76/// multiplicatively into [`rolled_throughput_yield`] — measuring it only at
77/// final delivery hides rework introduced and caught earlier in the stream.
78///
79/// # Arguments
80///
81/// * `usable_without_rework` — units the downstream stage can process
82///   without sending them back.
83/// * `total_units` — total units the stage produced.
84///
85/// # Returns
86///
87/// `Some(percentage)` (e.g. `90.0` for 90%), or `None` when `total_units`
88/// is zero.
89///
90/// # Examples
91///
92/// ```rust
93/// use software_engineering::lean_value_stream_metrics::percent_complete_and_accurate;
94///
95/// let pca = percent_complete_and_accurate(90.0, 100.0).unwrap();
96/// assert!((pca - 90.0).abs() < 1e-9);
97/// assert_eq!(percent_complete_and_accurate(90.0, 0.0), None);
98/// ```
99#[must_use]
100pub fn percent_complete_and_accurate(usable_without_rework: f64, total_units: f64) -> Option<f64> {
101    if total_units == 0.0 {
102        None
103    } else {
104        Some(usable_without_rework / total_units * 100.0)
105    }
106}
107
108/// Rolled throughput yield: the product of every stage's %C/A fraction
109/// across a multi-stage value stream.
110///
111/// Three stages each individually running at 90% complete-and-accurate
112/// compound to roughly 73% overall, a number that looks nothing like any
113/// single stage's own report and is usually the more honest one.
114///
115/// # Arguments
116///
117/// * `stage_pca_fractions` — each stage's %C/A expressed as a fraction
118///   (e.g. `0.9` for 90%), in stage order.
119///
120/// # Returns
121///
122/// The product of all fractions. An empty slice returns `1.0` (the identity
123/// for multiplication — no stages, no compounding loss).
124///
125/// # Examples
126///
127/// ```rust
128/// use software_engineering::lean_value_stream_metrics::rolled_throughput_yield;
129///
130/// let rty = rolled_throughput_yield(&[0.90, 0.90, 0.90]);
131/// assert!((rty - 0.729).abs() < 1e-9);
132///
133/// assert_eq!(rolled_throughput_yield(&[]), 1.0);
134/// ```
135#[must_use]
136pub fn rolled_throughput_yield(stage_pca_fractions: &[f64]) -> f64 {
137    stage_pca_fractions.iter().product()
138}
139
140/// Takt time: the maximum acceptable time to complete a unit to cleanly
141/// match customer demand.
142///
143/// Calculate takt time from real customer demand data, deliberately
144/// independent of how fast the team happens to be able to work today. A
145/// cycle time exceeding takt time is concrete, quantified evidence of a
146/// capacity shortfall.
147///
148/// # Arguments
149///
150/// * `available_working_time` — total working time available in the period
151///   (any consistent time unit).
152/// * `customer_demand` — number of units demanded over that same period.
153///
154/// # Returns
155///
156/// `Some(takt time)` per unit, or `None` when `customer_demand` is zero.
157///
158/// # Examples
159///
160/// ```rust
161/// use software_engineering::lean_value_stream_metrics::takt_time;
162///
163/// // 400 minutes of available time to meet demand for 20 units: 20 min/unit.
164/// assert_eq!(takt_time(400.0, 20.0), Some(20.0));
165/// assert_eq!(takt_time(400.0, 0.0), None);
166/// ```
167#[must_use]
168pub fn takt_time(available_working_time: f64, customer_demand: f64) -> Option<f64> {
169    if customer_demand == 0.0 {
170        None
171    } else {
172        Some(available_working_time / customer_demand)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    // Worked example: "Three stages each individually running at 90%
181    // complete and accurate compound to roughly 73% overall" (chapter 2.8).
182    #[test]
183    fn three_stages_at_90_percent_compound_to_about_73_percent() {
184        let rty = rolled_throughput_yield(&[0.90, 0.90, 0.90]);
185        assert!((rty - 0.729).abs() < 1e-9);
186    }
187
188    // Worked example: a four-stage pipeline where stages that individually
189    // look reasonable (95%, 90%, 85%, 83%) compound to a rolled throughput
190    // yield well below any single stage.
191    #[test]
192    fn rolled_throughput_yield_of_61_percent_is_below_any_single_stage() {
193        // Four stages that individually look reasonable but compound low.
194        let rty = rolled_throughput_yield(&[0.95, 0.90, 0.85, 0.83]);
195        assert!(rty < 0.90);
196        assert!((rty - 0.603_202_5).abs() < 1e-9);
197    }
198
199    #[test]
200    fn empty_rolled_throughput_yield_is_identity() {
201        assert!((rolled_throughput_yield(&[]) - 1.0).abs() < 1e-9);
202    }
203
204    #[test]
205    fn percent_complete_and_accurate_divides_correctly() {
206        let pca = percent_complete_and_accurate(90.0, 100.0).unwrap();
207        assert!((pca - 90.0).abs() < 1e-9);
208        assert_eq!(percent_complete_and_accurate(90.0, 0.0), None);
209    }
210
211    #[test]
212    fn takt_time_divides_available_time_by_demand() {
213        assert_eq!(takt_time(400.0, 20.0), Some(20.0));
214        assert_eq!(takt_time(400.0, 0.0), None);
215    }
216}