oee 0.1.0__py3-none-any.whl
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.
- oee/__init__.py +32 -0
- oee/_result.py +146 -0
- oee/_version.py +1 -0
- oee/aggregate.py +75 -0
- oee/core.py +165 -0
- oee/py.typed +0 -0
- oee-0.1.0.dist-info/METADATA +141 -0
- oee-0.1.0.dist-info/RECORD +11 -0
- oee-0.1.0.dist-info/WHEEL +5 -0
- oee-0.1.0.dist-info/licenses/LICENSE +21 -0
- oee-0.1.0.dist-info/top_level.txt +1 -0
oee/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""oee - Overall Equipment Effectiveness for Python.
|
|
2
|
+
|
|
3
|
+
Compute OEE (Availability x Performance x Quality) from machine times and piece
|
|
4
|
+
counts, get the full time waterfall and the three loss categories, TEEP and
|
|
5
|
+
utilization, and roll figures up correctly across machines and shifts. Every
|
|
6
|
+
result carries provenance and a JSON-safe payload.
|
|
7
|
+
|
|
8
|
+
import oee
|
|
9
|
+
|
|
10
|
+
r = oee.oee(
|
|
11
|
+
planned_production_time=420, # minutes
|
|
12
|
+
downtime=47,
|
|
13
|
+
ideal_cycle_time=1 / 60, # minutes per piece
|
|
14
|
+
total_count=19271,
|
|
15
|
+
reject_count=423,
|
|
16
|
+
)
|
|
17
|
+
r.oee # 0.7479
|
|
18
|
+
r.summary() # the factor and loss breakdown
|
|
19
|
+
|
|
20
|
+
Computed from the standard definitions and validated against published worked
|
|
21
|
+
examples.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ._result import Alert, OEEResult
|
|
25
|
+
from ._version import __version__
|
|
26
|
+
from .aggregate import aggregate
|
|
27
|
+
from .core import oee, oee_from_factors
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"oee", "oee_from_factors", "aggregate",
|
|
31
|
+
"OEEResult", "Alert", "__version__",
|
|
32
|
+
]
|
oee/_result.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The result contract: one ``OEEResult`` per computation, carrying the three
|
|
2
|
+
factors, the time waterfall, the loss categories, provenance and a JSON-safe
|
|
3
|
+
payload, so a calculation can be reproduced and audited later.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
SCHEMA = 1
|
|
14
|
+
WORLD_CLASS_OEE = 0.85
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def utcnow() -> str:
|
|
18
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def data_hash(obj: object) -> str:
|
|
22
|
+
payload = json.dumps(obj, sort_keys=True, default=str).encode("utf-8")
|
|
23
|
+
return "sha256:" + hashlib.sha256(payload).hexdigest()[:16]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Alert:
|
|
28
|
+
"""A data-quality or benchmark note attached to a result."""
|
|
29
|
+
|
|
30
|
+
indicator: str
|
|
31
|
+
message: str
|
|
32
|
+
severity: str = "warning" # info | warning | error
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"[{self.severity.upper()}] {self.indicator}: {self.message}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _pct(value: float | None) -> float | None:
|
|
39
|
+
return None if value is None else round(100.0 * value, 1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class OEEResult:
|
|
44
|
+
"""Overall Equipment Effectiveness and the time/loss breakdown behind it."""
|
|
45
|
+
|
|
46
|
+
name: str | None
|
|
47
|
+
availability: float
|
|
48
|
+
performance: float
|
|
49
|
+
quality: float
|
|
50
|
+
oee: float
|
|
51
|
+
performance_raw: float
|
|
52
|
+
utilization: float | None
|
|
53
|
+
teep: float | None
|
|
54
|
+
planned_production_time: float | None
|
|
55
|
+
run_time: float | None
|
|
56
|
+
net_run_time: float | None
|
|
57
|
+
fully_productive_time: float | None
|
|
58
|
+
all_time: float | None
|
|
59
|
+
schedule_loss: float | None
|
|
60
|
+
availability_loss: float | None
|
|
61
|
+
performance_loss: float | None
|
|
62
|
+
quality_loss: float | None
|
|
63
|
+
total_count: float | None
|
|
64
|
+
good_count: float | None
|
|
65
|
+
reject_count: float | None
|
|
66
|
+
target_oee: float
|
|
67
|
+
alerts: tuple[Alert, ...]
|
|
68
|
+
meta: dict
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def world_class(self) -> bool:
|
|
72
|
+
"""True when OEE meets the widely cited world-class level of 85%."""
|
|
73
|
+
return self.oee >= WORLD_CLASS_OEE
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def meets_target(self) -> bool:
|
|
77
|
+
return self.oee >= self.target_oee
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def ok(self) -> bool:
|
|
81
|
+
"""True when no error-severity (data-quality) alert was raised."""
|
|
82
|
+
return not any(a.severity == "error" for a in self.alerts)
|
|
83
|
+
|
|
84
|
+
def _verdict(self) -> str:
|
|
85
|
+
if self.world_class:
|
|
86
|
+
return "world-class"
|
|
87
|
+
if self.meets_target:
|
|
88
|
+
return "meets target"
|
|
89
|
+
return "below target"
|
|
90
|
+
|
|
91
|
+
def summary(self) -> str:
|
|
92
|
+
head = "oee" + (f" {self.name}" if self.name else "")
|
|
93
|
+
lines = [
|
|
94
|
+
f"{head} - {self.meta.get('computed_at', '')}",
|
|
95
|
+
f" Availability {_pct(self.availability):>5}%",
|
|
96
|
+
f" Performance {_pct(self.performance):>5}%",
|
|
97
|
+
f" Quality {_pct(self.quality):>5}%",
|
|
98
|
+
f" OEE {_pct(self.oee):>5}%",
|
|
99
|
+
]
|
|
100
|
+
if self.teep is not None:
|
|
101
|
+
lines.append(f" TEEP {_pct(self.teep):>5}%")
|
|
102
|
+
for alert in self.alerts:
|
|
103
|
+
lines.append(" " + str(alert))
|
|
104
|
+
lines.append(f"Verdict: OEE {_pct(self.oee)}% - {self._verdict()}")
|
|
105
|
+
return "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict:
|
|
108
|
+
return {
|
|
109
|
+
"schema": SCHEMA,
|
|
110
|
+
"name": self.name,
|
|
111
|
+
"factors": {
|
|
112
|
+
"availability": self.availability,
|
|
113
|
+
"performance": self.performance,
|
|
114
|
+
"quality": self.quality,
|
|
115
|
+
"oee": self.oee,
|
|
116
|
+
"performance_raw": self.performance_raw,
|
|
117
|
+
"utilization": self.utilization,
|
|
118
|
+
"teep": self.teep,
|
|
119
|
+
},
|
|
120
|
+
"times": {
|
|
121
|
+
"all_time": self.all_time,
|
|
122
|
+
"planned_production_time": self.planned_production_time,
|
|
123
|
+
"run_time": self.run_time,
|
|
124
|
+
"net_run_time": self.net_run_time,
|
|
125
|
+
"fully_productive_time": self.fully_productive_time,
|
|
126
|
+
},
|
|
127
|
+
"losses": {
|
|
128
|
+
"schedule_loss": self.schedule_loss,
|
|
129
|
+
"availability_loss": self.availability_loss,
|
|
130
|
+
"performance_loss": self.performance_loss,
|
|
131
|
+
"quality_loss": self.quality_loss,
|
|
132
|
+
},
|
|
133
|
+
"counts": {
|
|
134
|
+
"total_count": self.total_count,
|
|
135
|
+
"good_count": self.good_count,
|
|
136
|
+
"reject_count": self.reject_count,
|
|
137
|
+
},
|
|
138
|
+
"world_class": self.world_class,
|
|
139
|
+
"meets_target": self.meets_target,
|
|
140
|
+
"target_oee": self.target_oee,
|
|
141
|
+
"alerts": [
|
|
142
|
+
{"indicator": a.indicator, "message": a.message, "severity": a.severity}
|
|
143
|
+
for a in self.alerts
|
|
144
|
+
],
|
|
145
|
+
"meta": self.meta,
|
|
146
|
+
}
|
oee/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
oee/aggregate.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Roll OEE up across machines, lines, shifts or periods.
|
|
2
|
+
|
|
3
|
+
OEE figures must not be averaged: a fast machine and a slow one do not combine
|
|
4
|
+
to the mean of their OEEs. The correct roll-up sums the underlying time and
|
|
5
|
+
count buckets and recomputes the factors from the totals, which this does.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._result import Alert, OEEResult, data_hash, utcnow
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
|
|
13
|
+
_REQUIRED = ("planned_production_time", "run_time", "net_run_time",
|
|
14
|
+
"fully_productive_time", "total_count", "good_count")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def aggregate(parts, *, target_oee: float = 0.85,
|
|
18
|
+
name: str | None = None) -> OEEResult:
|
|
19
|
+
"""Combine several OEEResults into one by summing their time/count buckets."""
|
|
20
|
+
parts = list(parts)
|
|
21
|
+
if not parts:
|
|
22
|
+
raise ValueError("aggregate needs at least one result")
|
|
23
|
+
for i, part in enumerate(parts):
|
|
24
|
+
if not isinstance(part, OEEResult):
|
|
25
|
+
raise TypeError("aggregate expects OEEResult instances")
|
|
26
|
+
for field in _REQUIRED:
|
|
27
|
+
if getattr(part, field) is None:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"part {i} has no {field}; aggregate needs results created "
|
|
30
|
+
"from times and counts (oee(), not oee_from_factors())")
|
|
31
|
+
|
|
32
|
+
s_planned = sum(p.planned_production_time for p in parts)
|
|
33
|
+
s_run = sum(p.run_time for p in parts)
|
|
34
|
+
s_net = sum(p.net_run_time for p in parts)
|
|
35
|
+
s_fp = sum(p.fully_productive_time for p in parts)
|
|
36
|
+
s_total = sum(p.total_count for p in parts)
|
|
37
|
+
s_good = sum(p.good_count for p in parts)
|
|
38
|
+
|
|
39
|
+
availability = s_run / s_planned if s_planned else 0.0
|
|
40
|
+
performance = s_net / s_run if s_run else 0.0
|
|
41
|
+
quality = s_fp / s_net if s_net else 0.0
|
|
42
|
+
oee_value = s_fp / s_planned if s_planned else 0.0
|
|
43
|
+
|
|
44
|
+
if all(p.all_time is not None for p in parts):
|
|
45
|
+
s_all = sum(p.all_time for p in parts)
|
|
46
|
+
utilization = s_planned / s_all if s_all else 0.0
|
|
47
|
+
teep = s_fp / s_all if s_all else 0.0
|
|
48
|
+
schedule_loss = s_all - s_planned
|
|
49
|
+
else:
|
|
50
|
+
s_all = utilization = teep = schedule_loss = None
|
|
51
|
+
|
|
52
|
+
alerts: list[Alert] = []
|
|
53
|
+
if oee_value < target_oee:
|
|
54
|
+
alerts.append(Alert(
|
|
55
|
+
"oee", f"OEE {oee_value * 100:.1f}% is below the target "
|
|
56
|
+
f"{target_oee * 100:.0f}%", "info"))
|
|
57
|
+
|
|
58
|
+
meta = {
|
|
59
|
+
"computed_at": utcnow(),
|
|
60
|
+
"version": __version__,
|
|
61
|
+
"method": "aggregate",
|
|
62
|
+
"parts": len(parts),
|
|
63
|
+
"input_hash": data_hash([p.meta.get("input_hash") for p in parts]),
|
|
64
|
+
}
|
|
65
|
+
return OEEResult(
|
|
66
|
+
name=name, availability=availability, performance=performance,
|
|
67
|
+
quality=quality, oee=oee_value, performance_raw=performance,
|
|
68
|
+
utilization=utilization, teep=teep,
|
|
69
|
+
planned_production_time=s_planned, run_time=s_run, net_run_time=s_net,
|
|
70
|
+
fully_productive_time=s_fp, all_time=s_all, schedule_loss=schedule_loss,
|
|
71
|
+
availability_loss=s_planned - s_run, performance_loss=s_run - s_net,
|
|
72
|
+
quality_loss=s_net - s_fp, total_count=s_total, good_count=s_good,
|
|
73
|
+
reject_count=s_total - s_good, target_oee=target_oee,
|
|
74
|
+
alerts=tuple(alerts), meta=meta,
|
|
75
|
+
)
|
oee/core.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Overall Equipment Effectiveness from the standard definitions.
|
|
2
|
+
|
|
3
|
+
OEE = Availability x Performance x Quality, with
|
|
4
|
+
|
|
5
|
+
Availability = run time / planned production time
|
|
6
|
+
Performance = (ideal cycle time x total count) / run time
|
|
7
|
+
Quality = good count / total count
|
|
8
|
+
|
|
9
|
+
All times must be in the same unit, and ``ideal_cycle_time`` in that unit per
|
|
10
|
+
piece (or pass ``ideal_rate`` in pieces per that unit). The full time waterfall
|
|
11
|
+
(planned -> run -> net run -> fully productive) and each loss are returned, plus
|
|
12
|
+
TEEP and utilization when ``all_time`` is given.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
|
|
19
|
+
from ._result import Alert, OEEResult, data_hash, utcnow
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _finite(value: float, label: str) -> float:
|
|
24
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
25
|
+
raise TypeError(f"{label} must be a number")
|
|
26
|
+
value = float(value)
|
|
27
|
+
if not math.isfinite(value):
|
|
28
|
+
raise ValueError(f"{label} must be finite")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _exactly_one(a, b, name_a: str, name_b: str):
|
|
33
|
+
if (a is None) == (b is None):
|
|
34
|
+
raise ValueError(f"provide exactly one of {name_a} or {name_b}")
|
|
35
|
+
return a, b
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def oee(planned_production_time, *, run_time=None, downtime=None,
|
|
39
|
+
ideal_cycle_time=None, ideal_rate=None, total_count,
|
|
40
|
+
good_count=None, reject_count=None, all_time=None,
|
|
41
|
+
target_oee: float = 0.85, name: str | None = None) -> OEEResult:
|
|
42
|
+
"""Compute OEE and the time/loss waterfall from times and piece counts."""
|
|
43
|
+
planned = _finite(planned_production_time, "planned_production_time")
|
|
44
|
+
if planned <= 0:
|
|
45
|
+
raise ValueError("planned_production_time must be positive")
|
|
46
|
+
|
|
47
|
+
_exactly_one(run_time, downtime, "run_time", "downtime")
|
|
48
|
+
run = _finite(run_time, "run_time") if run_time is not None \
|
|
49
|
+
else planned - _finite(downtime, "downtime")
|
|
50
|
+
if not 0 <= run <= planned:
|
|
51
|
+
raise ValueError("run_time must be between 0 and planned_production_time")
|
|
52
|
+
|
|
53
|
+
_exactly_one(ideal_cycle_time, ideal_rate, "ideal_cycle_time", "ideal_rate")
|
|
54
|
+
if ideal_cycle_time is not None:
|
|
55
|
+
cycle = _finite(ideal_cycle_time, "ideal_cycle_time")
|
|
56
|
+
else:
|
|
57
|
+
rate = _finite(ideal_rate, "ideal_rate")
|
|
58
|
+
if rate <= 0:
|
|
59
|
+
raise ValueError("ideal_rate must be positive")
|
|
60
|
+
cycle = 1.0 / rate
|
|
61
|
+
if cycle <= 0:
|
|
62
|
+
raise ValueError("ideal_cycle_time must be positive")
|
|
63
|
+
|
|
64
|
+
total = _finite(total_count, "total_count")
|
|
65
|
+
if total <= 0:
|
|
66
|
+
raise ValueError("total_count must be positive")
|
|
67
|
+
_exactly_one(good_count, reject_count, "good_count", "reject_count")
|
|
68
|
+
good = _finite(good_count, "good_count") if good_count is not None \
|
|
69
|
+
else total - _finite(reject_count, "reject_count")
|
|
70
|
+
if not 0 <= good <= total:
|
|
71
|
+
raise ValueError("good_count must be between 0 and total_count")
|
|
72
|
+
reject = total - good
|
|
73
|
+
|
|
74
|
+
alerts: list[Alert] = []
|
|
75
|
+
|
|
76
|
+
availability = run / planned if planned else 0.0
|
|
77
|
+
|
|
78
|
+
ideal_time_total = cycle * total
|
|
79
|
+
performance_raw = ideal_time_total / run if run > 0 else 0.0
|
|
80
|
+
if performance_raw > 1.0 + 1e-9:
|
|
81
|
+
performance = 1.0
|
|
82
|
+
net_run = run
|
|
83
|
+
alerts.append(Alert(
|
|
84
|
+
"performance", "performance exceeds 100%; capped at 100% (check "
|
|
85
|
+
"ideal_cycle_time / ideal_rate and counts)", "warning"))
|
|
86
|
+
else:
|
|
87
|
+
performance = performance_raw
|
|
88
|
+
net_run = ideal_time_total
|
|
89
|
+
|
|
90
|
+
quality = good / total
|
|
91
|
+
fully_productive = quality * net_run
|
|
92
|
+
oee_value = availability * performance * quality
|
|
93
|
+
|
|
94
|
+
schedule_loss = None
|
|
95
|
+
utilization = None
|
|
96
|
+
teep = None
|
|
97
|
+
if all_time is not None:
|
|
98
|
+
at = _finite(all_time, "all_time")
|
|
99
|
+
if at < planned:
|
|
100
|
+
raise ValueError("all_time must be at least planned_production_time")
|
|
101
|
+
schedule_loss = at - planned
|
|
102
|
+
utilization = planned / at if at > 0 else 0.0
|
|
103
|
+
teep = fully_productive / at if at > 0 else 0.0
|
|
104
|
+
|
|
105
|
+
if not 0 < target_oee <= 1:
|
|
106
|
+
raise ValueError("target_oee must be in (0, 1]")
|
|
107
|
+
if oee_value < target_oee:
|
|
108
|
+
alerts.append(Alert(
|
|
109
|
+
"oee", f"OEE {oee_value * 100:.1f}% is below the target "
|
|
110
|
+
f"{target_oee * 100:.0f}%", "info"))
|
|
111
|
+
|
|
112
|
+
meta = {
|
|
113
|
+
"computed_at": utcnow(),
|
|
114
|
+
"version": __version__,
|
|
115
|
+
"method": "time_and_counts",
|
|
116
|
+
"input_hash": data_hash({
|
|
117
|
+
"planned": planned, "run": run, "cycle": cycle,
|
|
118
|
+
"total": total, "good": good, "all_time": all_time,
|
|
119
|
+
}),
|
|
120
|
+
}
|
|
121
|
+
return OEEResult(
|
|
122
|
+
name=name, availability=availability, performance=performance,
|
|
123
|
+
quality=quality, oee=oee_value, performance_raw=performance_raw,
|
|
124
|
+
utilization=utilization, teep=teep,
|
|
125
|
+
planned_production_time=planned, run_time=run, net_run_time=net_run,
|
|
126
|
+
fully_productive_time=fully_productive, all_time=all_time,
|
|
127
|
+
schedule_loss=schedule_loss, availability_loss=planned - run,
|
|
128
|
+
performance_loss=run - net_run, quality_loss=net_run - fully_productive,
|
|
129
|
+
total_count=total, good_count=good, reject_count=reject,
|
|
130
|
+
target_oee=target_oee, alerts=tuple(alerts), meta=meta,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def oee_from_factors(availability, performance, quality, *,
|
|
135
|
+
target_oee: float = 0.85, name: str | None = None) -> OEEResult:
|
|
136
|
+
"""Compute OEE from the three factors directly (no times or counts)."""
|
|
137
|
+
a = _finite(availability, "availability")
|
|
138
|
+
p = _finite(performance, "performance")
|
|
139
|
+
q = _finite(quality, "quality")
|
|
140
|
+
alerts: list[Alert] = []
|
|
141
|
+
for label, value in (("availability", a), ("performance", p), ("quality", q)):
|
|
142
|
+
if not 0 <= value <= 1.0 + 1e-9:
|
|
143
|
+
raise ValueError(f"{label} must be between 0 and 1")
|
|
144
|
+
if not 0 < target_oee <= 1:
|
|
145
|
+
raise ValueError("target_oee must be in (0, 1]")
|
|
146
|
+
oee_value = a * p * q
|
|
147
|
+
if oee_value < target_oee:
|
|
148
|
+
alerts.append(Alert(
|
|
149
|
+
"oee", f"OEE {oee_value * 100:.1f}% is below the target "
|
|
150
|
+
f"{target_oee * 100:.0f}%", "info"))
|
|
151
|
+
meta = {
|
|
152
|
+
"computed_at": utcnow(),
|
|
153
|
+
"version": __version__,
|
|
154
|
+
"method": "factors",
|
|
155
|
+
"input_hash": data_hash({"a": a, "p": p, "q": q}),
|
|
156
|
+
}
|
|
157
|
+
return OEEResult(
|
|
158
|
+
name=name, availability=a, performance=p, quality=q, oee=oee_value,
|
|
159
|
+
performance_raw=p, utilization=None, teep=None,
|
|
160
|
+
planned_production_time=None, run_time=None, net_run_time=None,
|
|
161
|
+
fully_productive_time=None, all_time=None, schedule_loss=None,
|
|
162
|
+
availability_loss=None, performance_loss=None, quality_loss=None,
|
|
163
|
+
total_count=None, good_count=None, reject_count=None,
|
|
164
|
+
target_oee=target_oee, alerts=tuple(alerts), meta=meta,
|
|
165
|
+
)
|
oee/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oee
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Overall Equipment Effectiveness for Python: availability, performance and quality with the full time waterfall, TEEP, and correct multi-machine roll-up. Computed from the standard definitions and validated against published examples.
|
|
5
|
+
Author: Atakan Arikan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arikanatakan/oee
|
|
8
|
+
Project-URL: Repository, https://github.com/arikanatakan/oee
|
|
9
|
+
Project-URL: Issues, https://github.com/arikanatakan/oee/issues
|
|
10
|
+
Keywords: oee,overall-equipment-effectiveness,manufacturing,tpm,teep,availability,performance,quality,six-big-losses,production,lean
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Manufacturing
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# oee
|
|
30
|
+
|
|
31
|
+
[](https://github.com/arikanatakan/oee/actions/workflows/ci.yml)
|
|
32
|
+
[](https://pypi.org/project/oee/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
Overall Equipment Effectiveness for Python.
|
|
36
|
+
|
|
37
|
+
Compute OEE (Availability x Performance x Quality) from machine times and piece
|
|
38
|
+
counts, get the full time waterfall and the three loss categories, TEEP and
|
|
39
|
+
utilization, and roll figures up correctly across machines and shifts. Computed
|
|
40
|
+
from the standard definitions and validated against published worked examples.
|
|
41
|
+
|
|
42
|
+
## Motivation
|
|
43
|
+
|
|
44
|
+
OEE is the standard manufacturing efficiency metric, but Python has no library
|
|
45
|
+
for it: what exists is monitoring *applications* (Flask/Django dashboards) or
|
|
46
|
+
one-off tutorial scripts. The arithmetic looks trivial (three numbers
|
|
47
|
+
multiplied) and that is exactly why it is usually done wrong:
|
|
48
|
+
|
|
49
|
+
* the time waterfall (planned -> run -> net run -> fully productive) and where
|
|
50
|
+
each loss sits is skipped;
|
|
51
|
+
* TEEP and utilization (which capture schedule loss) are left out;
|
|
52
|
+
* and figures are *averaged* across machines, which is incorrect: a fast machine
|
|
53
|
+
and a slow one do not combine to the mean of their OEEs.
|
|
54
|
+
|
|
55
|
+
`oee` does these properly, from the standard definitions, and returns one result
|
|
56
|
+
with the factors, the waterfall, every loss, and provenance.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
pip install oee
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
No runtime dependencies.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
The canonical worked example (Vorne's *Fast Guide to OEE*):
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import oee
|
|
70
|
+
|
|
71
|
+
r = oee.oee(
|
|
72
|
+
planned_production_time=420, # minutes (480 shift - 60 of breaks)
|
|
73
|
+
downtime=47,
|
|
74
|
+
ideal_rate=60, # pieces per minute
|
|
75
|
+
total_count=19271,
|
|
76
|
+
reject_count=423,
|
|
77
|
+
all_time=480, # optional, for TEEP and utilization
|
|
78
|
+
)
|
|
79
|
+
r.availability # 0.888
|
|
80
|
+
r.performance # 0.861
|
|
81
|
+
r.quality # 0.978
|
|
82
|
+
r.oee # 0.748
|
|
83
|
+
r.teep # 0.654
|
|
84
|
+
print(r.summary())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Roll up across machines (correctly, not by averaging):
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
m1 = oee.oee(planned_production_time=100, run_time=90, ideal_cycle_time=1,
|
|
91
|
+
total_count=80, good_count=80) # OEE 0.80
|
|
92
|
+
m2 = oee.oee(planned_production_time=300, run_time=150, ideal_cycle_time=1,
|
|
93
|
+
total_count=150, good_count=135) # OEE 0.45
|
|
94
|
+
|
|
95
|
+
line = oee.aggregate([m1, m2])
|
|
96
|
+
line.oee # 0.5375, not the 0.625 average of the two
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When you already have the three factors:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
oee.oee_from_factors(0.90, 0.95, 0.999).world_class # True (OEE >= 85%)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Every result carries the factors, the time waterfall, the losses, `world_class`
|
|
106
|
+
and `meets_target` flags, `summary()`, and a JSON-safe `to_dict()` with
|
|
107
|
+
provenance (version, input hash, timestamp).
|
|
108
|
+
|
|
109
|
+
## What it computes
|
|
110
|
+
|
|
111
|
+
| Group | Output |
|
|
112
|
+
|-------|--------|
|
|
113
|
+
| Factors | availability, performance, quality, OEE |
|
|
114
|
+
| Extended | TEEP, utilization (when total calendar time is given) |
|
|
115
|
+
| Waterfall | planned -> run -> net run -> fully productive time, with schedule, availability, performance and quality losses |
|
|
116
|
+
| Roll-up | correct aggregation across machines, lines and shifts |
|
|
117
|
+
|
|
118
|
+
All times must be in the same unit; `ideal_cycle_time` is that unit per piece
|
|
119
|
+
(or pass `ideal_rate` in pieces per that unit). Performance above 100% is capped
|
|
120
|
+
and flagged, since it means the ideal rate or counts are off.
|
|
121
|
+
|
|
122
|
+
## Status
|
|
123
|
+
|
|
124
|
+
Version 0.1.0. Single-machine OEE, the time waterfall, TEEP/utilization, and
|
|
125
|
+
correct roll-up. The `OEEResult` contract is append-only from here.
|
|
126
|
+
|
|
127
|
+
## Roadmap
|
|
128
|
+
|
|
129
|
+
| Version | Scope |
|
|
130
|
+
|---------|-------|
|
|
131
|
+
| 0.2 | the six big losses (breakdown/setup, minor stops/speed, defects/startup) and a downtime-reason Pareto; computing OEE from an event log |
|
|
132
|
+
| 0.3 | plotting (the OEE waterfall, six-big-losses and trend charts) as an optional extra |
|
|
133
|
+
| 0.4 | an MCP server so an agent can compute and explain OEE |
|
|
134
|
+
|
|
135
|
+
Out of scope: data collection / machine connectivity (that is the job of an
|
|
136
|
+
MES or an IoT dashboard); `oee` is the calculation layer they can build on.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
141
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
oee/__init__.py,sha256=wlKJcqRJR3eGdrWj5f3noaymay5QG6UoAsiRJlPyjaQ,982
|
|
2
|
+
oee/_result.py,sha256=qkfsayUZsTHYPeFLmV0vWC-g1QS9YBffnv2fEbgzm1I,4756
|
|
3
|
+
oee/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
4
|
+
oee/aggregate.py,sha256=k0pJvuDV0LDTBccyOfXaEqEUFu63ZhdudidQfJj-QzM,3152
|
|
5
|
+
oee/core.py,sha256=kmcNmIK6XfOXPNkaDXZ_eURid1z4WOn-gHG8iFmONdw,6661
|
|
6
|
+
oee/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
oee-0.1.0.dist-info/licenses/LICENSE,sha256=a3yEF0vGHVodoVsu7IKY7Otc_8-fuwfYXMMm9h3VoJg,1070
|
|
8
|
+
oee-0.1.0.dist-info/METADATA,sha256=wyBC1QENjIX-jCpj6EpNNHV-puBiIDD5FfnKtCbg6Lk,5503
|
|
9
|
+
oee-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
oee-0.1.0.dist-info/top_level.txt,sha256=_pkBHsyLovAxUFoUICE8FzpdhvXv9bSNTtBBgaCPj1g,4
|
|
11
|
+
oee-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atakan Arikan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oee
|