Skip to content

Commit dd8ba92

Browse files
Producer GuyProducer Guy
authored andcommitted
Fix all 12 audit issues from fan-first calibration review
Major — documentation rewritten for fan-first approach: - README calibration section: describes stabilization sweep, not binary search - README modes table: shows stabilization window and max wait, not iterations - ROADMAP calibration section: describes fan-first with control curve formula - ROADMAP header: calibration redesign marked complete Moderate: - CalibrationMode.description timing corrected (17/25/35 min) - CLI print: "5 fan speed levels" (was 4) Code quality: - Removed dead performanceCeiling constant (84°C ceilingTemp is used) - Fixed misplaced doc comment on isStabilized - Removed orphaned doc comment - Fixed Metal resource leak in CalibrationView (nil out on stop) - CalibrationMode doc comments: match actual maxWaitPerLevel values
1 parent 6701831 commit dd8ba92

5 files changed

Lines changed: 43 additions & 44 deletions

File tree

README.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -138,47 +138,45 @@ Apple doesn't do this because silence sells in store demos and most users never
138138
For best results, calibrate Smart to your specific machine:
139139

140140
```bash
141-
sudo thermalforge calibrate # Standard (~25 min)
142-
sudo thermalforge calibrate --mode quick # Quick (~15 min)
143-
sudo thermalforge calibrate --mode optimized # Optimized (~40 min)
141+
sudo thermalforge calibrate # Standard (up to 25 min)
142+
sudo thermalforge calibrate --mode quick # Quick (up to 17 min)
143+
sudo thermalforge calibrate --mode optimized # Optimized (up to 35 min)
144144
```
145145

146-
Calibration answers one question per temperature point: **what fan speed holds this machine at this temperature?**
146+
Calibration answers one question per fan speed: **what temperature does this machine stabilize at with fans at X%?**
147147

148148
It works in three phases:
149149

150150
1. **Discover intensity** — finds the stress level that heats your machine at ~1°C/sec (matching real workloads, not synthetic maximum). Uses Metal compute + CPU stress.
151-
2. **Heat to target**runs stress with fans on auto, lets the machine warm naturally to each target temperature (60°C, 65°C, 70°C, 75°C, 80°C, 85°C).
152-
3. **Find holding speed**at each target, binary-searches for the fan speed that stabilizes temperature there. Tests a fan speed, measures whether temp is rising/falling/stable, adjusts up or down until it converges.
151+
2. **Fan-level stabilization sweep**sets fans to 100%, applies calibrated stress, and waits for temperature to stabilize. Then reduces fans to 80% and waits again. Repeats at 60%, 45%, and minimum. At each level, the equilibrium temperature is recorded.
152+
3. **Build control curve**transforms the raw data (higher fan = lower equilibrium temp) into a control curve (higher temp = more fan needed) that Smart reads at runtime.
153153

154154
The result is a temperature-to-fan-speed lookup table specific to your machine:
155155

156156
```
157-
60°C → 30% fans
158-
65°C → 42% fans
159-
70°C → 55% fans
160-
75°C → 68% fans
161-
80°C → 82% fans
162-
85°C → 96% fans
157+
60°C → 39% fans
158+
65°C → 53% fans
159+
70°C → 65% fans
160+
75°C → 78% fans
161+
80°C → 90% fans
162+
85°C → 100% fans
163163
```
164164

165-
Smart reads this table and interpolates for any temperature. At 72°C on your machine, Smart knows it needs ~58% fan speed — not a guess, a measurement.
165+
Smart reads this table and interpolates for any temperature. At 72°C on your machine, Smart knows it needs ~70% fan speed — not a guess, a measurement.
166166

167-
The machine never exceeds the target temperature because calibration controls the target, not the fan speed. A 95°C safety backstop is always active as a failsafe.
167+
Temperature is protected by three layers: an 84°C ceiling (skips lower fan levels), a 90°C safety stop (maxes fans immediately), and a 95°C backstop (always active regardless of what's running).
168168

169169
### Calibration modes
170170

171-
Modes control how many binary search iterations per target and how long each test holds:
171+
Modes control how long the machine waits for thermal equilibrium at each fan level:
172172

173-
| Mode | Time | Binary search iterations | Hold time per test |
173+
| Mode | Time | Stabilization window | Max wait per level |
174174
|---|---|---|---|
175-
| **Quick** | ~15 min | 4 per target | 8 seconds |
176-
| **Standard** | ~25 min | 6 per target | 15 seconds |
177-
| **Optimized** | ~40 min | 8 per target | 25 seconds |
175+
| **Quick** | up to 17 min | 60 seconds (30 readings) | 2.5 minutes |
176+
| **Standard** | up to 25 min | 90 seconds (45 readings) | 4 minutes |
177+
| **Optimized** | up to 35 min | 120 seconds (60 readings) | 6 minutes |
178178

179-
More iterations and longer hold times produce more accurate data. Standard is recommended for most users.
180-
181-
Timing is based on measured thermal time constants of 90-120 seconds for Apple Silicon laptop heatsink assemblies (Notebookcheck M1-M4 MacBook Pro stress tests, [Max Tech](https://www.youtube.com/@MaxTech) sustained performance testing). Mac Studio's larger thermal mass (~2-3x) is covered by Standard mode's timing.
179+
Longer stabilization windows produce more accurate equilibrium measurements. Standard is recommended for most users. Timing is based on measured thermal time constants of 90-120 seconds for Apple Silicon laptop heatsink assemblies (Notebookcheck M1-M4 MacBook Pro stress tests, [Max Tech](https://www.youtube.com/@MaxTech) sustained performance testing).
182180

183181
**Smart works without calibration** — it uses a conservative default curve. Calibration makes it precise for your hardware.
184182

ROADMAP.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ Proactive thermal curve that monitors temperature velocity and ramps fans before
1515

1616
### Calibration (Built)
1717

18-
Temp-first machine-specific thermal profiling. Heats to target temperatures, binary-searches for the fan speed that holds each one.
19-
20-
- Adaptive intensity finder: discovers the ~1°C/sec stress level for this machine before calibration
21-
- 6 target temperatures: 60, 65, 70, 75, 80, 85°C
22-
- At each target: binary search for holding fan speed (4-8 iterations per target depending on mode)
23-
- Produces temp→fan lookup table: "60°C needs 30% fans, 70°C needs 55% fans, ..."
24-
- Machine never exceeds target temp — we control the target, not the fan speed
25-
- 95°C safety backstop always active during both heating and search phases
26-
- Three modes: Quick (~15 min), Standard (~25 min), Optimized (~40 min)
18+
Fan-first stabilization sweep. Sets fan speed, applies calibrated stress, waits for thermal equilibrium, records the stabilization temperature.
19+
20+
- Adaptive intensity finder: discovers the ~1°C/sec stress level for this machine
21+
- 5 fan levels swept high to low: 100% → 80% → 60% → 45% → minimum
22+
- At each level: wait for temperature to stabilize (stdev < 0.5°C AND slope < 0.05°C/sec)
23+
- Raw data transformed to monotonically increasing control curve via `fan_control(T) = (1.0 + minPct) - F_equil(T)`
24+
- Smart reads the curve via `fanPercentForTemp()` with interpolation
25+
- Protection: 84°C ceiling (skip lower levels), 90°C safety (max fans), 95°C backstop (always active)
26+
- Three modes: Quick (up to 17 min), Standard (up to 25 min), Optimized (up to 35 min)
2727
- CPU+GPU combined, CPU only, or GPU only stress types
2828
- Downgrade prevention: Quick can't overwrite Standard or Optimized
2929
- In-app UI: mode picker, progress bar, live temp, stop button
@@ -61,7 +61,7 @@ Research-grade data export: `thermalforge log`
6161

6262
---
6363

64-
## Profile + Smart Redesign (Complete) + Calibration Redesign (Pending)
64+
## Profile + Smart + Calibration Redesign (Complete)
6565

6666
The entire profile system, Smart curve, and calibration need to be redesigned as one cohesive system. Current profiles are binary switches that immediately set fans to a fixed percentage. They should be proportional curves that respect Apple's fan hardware behavior.
6767

Sources/ThermalForgeApp/CalibrationView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ final class CalibrationState: ObservableObject {
6464
activeStressFlag?.stop()
6565
activeStressFlag = nil
6666

67+
// Release Metal resources
68+
gpuPipeline = nil
69+
gpuQueue = nil
70+
gpuBuffer = nil
71+
gpuElementCount = 0
72+
6773
// Cancel async tasks
6874
task?.cancel()
6975
timerTask?.cancel()

Sources/ThermalForgeCore/Calibration.swift

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ public struct CalibrationData: Codable {
107107
}
108108

109109
public var isValid: Bool { validationError == nil }
110-
111-
/// Interpolate the RPM percentage needed to hold a target temperature under load.
112110
}
113111

114112
// MARK: - Persistence
@@ -167,23 +165,23 @@ extension CalibrationData {
167165
/// ~20-50 J/K × 0.3-0.8 K/W = 60-180s for laptop heatsink assemblies
168166
/// - 3 time constants = 95% of steady state, 5 time constants = 99.3%
169167
public enum CalibrationMode: String, CaseIterable {
170-
/// 5 fan levels × ~100s each + intensity finding + cooldowns ≈ 15 min
168+
/// 5 fan levels × up to 2.5 min each + intensity finding + cooldowns
171169
/// 60-second stabilization window (~80% accuracy)
172170
case quick
173171

174-
/// 5 fan levels × ~150s each + overhead ≈ 22 min
172+
/// 5 fan levels × up to 4 min each + overhead
175173
/// 90-second window (near one time constant, ~90% accuracy)
176174
case standard
177175

178-
/// 5 fan levels × ~200s each + overhead ≈ 30 min
176+
/// 5 fan levels × up to 6 min each + overhead
179177
/// 120-second window (full time constant, ~95% accuracy)
180178
case optimized
181179

182180
public var description: String {
183181
switch self {
184-
case .quick: return "Quick (up to 15 min)"
185-
case .standard: return "Standard (up to 22 min)"
186-
case .optimized: return "Optimized (up to 30 min)"
182+
case .quick: return "Quick (up to 17 min)"
183+
case .standard: return "Standard (up to 25 min)"
184+
case .optimized: return "Optimized (up to 35 min)"
187185
}
188186
}
189187

@@ -274,8 +272,6 @@ public final class CalibrationRunner {
274272
}
275273

276274
/// Performance ceiling — stop increasing load if temp reaches this
277-
private static let performanceCeiling: Float = 85.0
278-
279275
/// Target heating rate for calibration — matches real-world workloads.
280276
/// Research: real workloads heat Apple Silicon at ~1-2°C/sec.
281277
/// Max synthetic load heats at ~5-8°C/sec (Notebookcheck, Max Tech).
@@ -516,7 +512,6 @@ public final class CalibrationRunner {
516512
)
517513
}
518514

519-
/// Wait for machine to cool below a threshold
520515
/// Check if temperature readings have stabilized.
521516
/// Stable = stdev < 0.5°C AND slope < 0.05°C/sec over the window.
522517
private func isStabilized(readings: [Float]) -> Bool {

Sources/thermalforge/ThermalForge.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ struct Calibrate: ParsableCommand {
341341
print("Mode: \(calMode.description)")
342342
print("Stress: \(calStress.description)")
343343
print("")
344-
print("This will stress your \(calStress == .combined ? "CPU and GPU" : calStress == .cpu ? "CPU" : "GPU") and measure thermal response at 4 fan speed levels.")
344+
print("This will stress your \(calStress == .combined ? "CPU and GPU" : calStress == .cpu ? "CPU" : "GPU") and measure thermal response at 5 fan speed levels.")
345345
print("Fans will be loud during the test.")
346346
print("")
347347
print("DISCLAIMER: Calibration pushes your Mac to full load and cycles fan speeds.")

0 commit comments

Comments
 (0)