8453449833
Replace the inaccurate CFM-based formula with nozzle-primary tables sourced from industry standard abrasive blast cleaning references: - Pressure pot: midpoints averaged from two reference tables (#4 nozzle: 115 sqft/hr, #5: 175 sqft/hr, etc.) - Siphon cabinet: dedicated siphon cabinet reference table (#4 nozzle: 125 sqft/hr, #3: 75 sqft/hr, etc.) - SiphonPot: 80% of pressure pot rate (open gravity feed, no enclosure) - WetBlasting: 60% of pressure pot rate (water-media reduces velocity) CFM is removed from the rate formula entirely — nozzle size determines throughput and CFM draw, so CFM is a consequence of nozzle choice, not an independent variable. Override field still bypasses formula for shops that have measured their own throughput. Also corrects TierDefaults nozzle/CFM pairings which were mismatched (e.g. Small tier had 40 CFM assigned to a #5 nozzle that needs 150 CFM). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
7.3 KiB
C#
170 lines
7.3 KiB
C#
using PowderCoating.Core.Entities;
|
|
using PowderCoating.Core.Enums;
|
|
|
|
namespace PowderCoating.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Derives sqft/hr throughput rates from a shop's equipment configuration.
|
|
/// Used by the AI photo quote prompt (so Claude reasons from real shop speeds)
|
|
/// and the Company Settings live preview (so the UI always shows the same rate
|
|
/// the AI will use — single formula path, no client-side duplication).
|
|
///
|
|
/// Both pressure pots and siphon cabinets are nozzle-primary: nozzle size
|
|
/// determines throughput and CFM draw. CFM is not used in the rate formula.
|
|
///
|
|
/// Sources:
|
|
/// Pressure pot rates — averaged from two industry standard abrasive blast
|
|
/// cleaning reference tables.
|
|
/// Siphon cabinet rates — industry reference table for siphon-fed cabinets.
|
|
/// Substrate multipliers — relative removal difficulty vs. paint baseline.
|
|
/// </summary>
|
|
public static class ShopCapabilityCalculator
|
|
{
|
|
// ── Public entry points ────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Returns the effective blast rate in sqft/hr for company-level operating costs.
|
|
/// BlastRateSqFtPerHourOverride bypasses the formula when set.
|
|
/// </summary>
|
|
public static decimal GetBlastRateSqFtPerHour(CompanyOperatingCosts costs)
|
|
{
|
|
if (costs.BlastRateSqFtPerHourOverride.HasValue && costs.BlastRateSqFtPerHourOverride.Value > 0)
|
|
return costs.BlastRateSqFtPerHourOverride.Value;
|
|
|
|
return CalculateBlastRate(costs.BlastNozzleSize, costs.BlastSetupType, costs.PrimaryBlastSubstrate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the effective blast rate in sqft/hr for a named blast setup.
|
|
/// BlastRateSqFtPerHourOverride bypasses the formula when set.
|
|
/// </summary>
|
|
public static decimal GetBlastRateSqFtPerHour(CompanyBlastSetup setup)
|
|
{
|
|
if (setup.BlastRateSqFtPerHourOverride.HasValue && setup.BlastRateSqFtPerHourOverride.Value > 0)
|
|
return setup.BlastRateSqFtPerHourOverride.Value;
|
|
|
|
return CalculateBlastRate(setup.BlastNozzleSize, setup.SetupType, setup.PrimarySubstrate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the effective coating application rate in sqft/hr.
|
|
/// Override bypasses the formula when set.
|
|
/// </summary>
|
|
public static decimal GetCoatingRateSqFtPerHour(CompanyOperatingCosts costs)
|
|
{
|
|
if (costs.CoatingRateSqFtPerHourOverride.HasValue && costs.CoatingRateSqFtPerHourOverride.Value > 0)
|
|
return costs.CoatingRateSqFtPerHourOverride.Value;
|
|
|
|
return costs.CoatingGunType switch
|
|
{
|
|
CoatingGunType.Corona => 40m,
|
|
CoatingGunType.Tribo => 35m,
|
|
CoatingGunType.Both => 40m,
|
|
_ => 40m
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns default equipment field values for a given capability tier, applied
|
|
/// during Setup Wizard tier selection so new shops get reasonable starting values.
|
|
/// CFM defaults reflect typical compressor sizes for each tier; they appear in the
|
|
/// UI for reference but are not used in the rate formula.
|
|
/// </summary>
|
|
public static (BlastSetupType SetupType, decimal Cfm, int NozzleSize, BlastSubstrateType Substrate)
|
|
TierDefaults(ShopCapabilityTier tier) => tier switch
|
|
{
|
|
ShopCapabilityTier.Garage => (BlastSetupType.SiphonCabinet, 7m, 3, BlastSubstrateType.Mixed),
|
|
ShopCapabilityTier.Small => (BlastSetupType.PressurePot, 49m, 3, BlastSubstrateType.Mixed),
|
|
ShopCapabilityTier.Medium => (BlastSetupType.PressurePot, 90m, 4, BlastSubstrateType.Mixed),
|
|
ShopCapabilityTier.Large => (BlastSetupType.PressurePot, 150m, 5, BlastSubstrateType.Mixed),
|
|
_ => (BlastSetupType.SiphonCabinet, 7m, 3, BlastSubstrateType.Mixed)
|
|
};
|
|
|
|
// ── Core formula (single path for all callers) ─────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Nozzle-primary blast rate calculation. Nozzle size determines throughput;
|
|
/// setup type routes to the appropriate reference table; substrate adjusts for
|
|
/// removal difficulty. CFM is not used — it is a consequence of nozzle choice,
|
|
/// not an independent variable in throughput.
|
|
/// </summary>
|
|
private static decimal CalculateBlastRate(int nozzle, BlastSetupType setupType, BlastSubstrateType substrate)
|
|
{
|
|
var baseRate = setupType switch
|
|
{
|
|
BlastSetupType.PressurePot => PressurePotRateByNozzle(nozzle),
|
|
BlastSetupType.SiphonCabinet => SiphonCabinetRateByNozzle(nozzle),
|
|
// Siphon pot: open gravity feed, no enclosure penalty, ~80% of pressure pot
|
|
BlastSetupType.SiphonPot => Math.Round(PressurePotRateByNozzle(nozzle) * 0.80m, 1),
|
|
// Wet blasting: water-media mix reduces impact velocity, ~60% of dry pressure pot
|
|
BlastSetupType.WetBlasting => Math.Round(PressurePotRateByNozzle(nozzle) * 0.60m, 1),
|
|
_ => 0m
|
|
};
|
|
|
|
return Math.Round(baseRate * SubstrateMultiplier(substrate), 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Midpoint cleaning rates for a pressure pot at adequate air supply, by nozzle size.
|
|
/// Averaged from two industry-standard abrasive blast cleaning reference tables.
|
|
/// #1 (1/16"): 20-35 sqft/hr avg → 20
|
|
/// #2 (1/8"): 40-60 sqft/hr avg → 40
|
|
/// #3 (3/16"): 60-85 sqft/hr avg → 75
|
|
/// #4 (1/4"): 90-110 sqft/hr avg → 115
|
|
/// #5 (5/16"): 130-160 sqft/hr avg → 175
|
|
/// #6 (3/8"): 180-230 sqft/hr avg → 245
|
|
/// #7 (7/16"): 240-300 sqft/hr avg → 325
|
|
/// #8 (1/2"): 320-400 sqft/hr avg → 430
|
|
/// </summary>
|
|
private static decimal PressurePotRateByNozzle(int nozzle) => nozzle switch
|
|
{
|
|
1 => 20m,
|
|
2 => 40m,
|
|
3 => 75m,
|
|
4 => 115m,
|
|
5 => 175m,
|
|
6 => 245m,
|
|
7 => 325m,
|
|
8 => 430m,
|
|
_ => 100m
|
|
};
|
|
|
|
/// <summary>
|
|
/// Midpoint cleaning rates for siphon-fed blast cabinets, by nozzle size.
|
|
/// Source: industry reference table for siphon cabinet production rates.
|
|
/// #1 (1/16"): 10-25 sqft/hr → 18
|
|
/// #2 (1/8"): 25-50 sqft/hr → 38
|
|
/// #3 (3/16"): 50-100 sqft/hr → 75
|
|
/// #4 (1/4"): 100-150 sqft/hr → 125
|
|
/// #5 (5/16"): 150-225 sqft/hr → 188
|
|
/// #6 (3/8"): 225-300 sqft/hr → 263
|
|
/// #7 (7/16"): 300-375 sqft/hr → 338
|
|
/// #8 (1/2"): 375-450 sqft/hr → 413
|
|
/// </summary>
|
|
private static decimal SiphonCabinetRateByNozzle(int nozzle) => nozzle switch
|
|
{
|
|
1 => 18m,
|
|
2 => 38m,
|
|
3 => 75m,
|
|
4 => 125m,
|
|
5 => 188m,
|
|
6 => 263m,
|
|
7 => 338m,
|
|
8 => 413m,
|
|
_ => 80m
|
|
};
|
|
|
|
/// <summary>
|
|
/// Adjustment for substrate removal difficulty relative to paint (baseline = 1.0).
|
|
/// Powder coat strips faster than paint; rust and scale requires multiple passes.
|
|
/// </summary>
|
|
private static decimal SubstrateMultiplier(BlastSubstrateType substrate) => substrate switch
|
|
{
|
|
BlastSubstrateType.PowderCoat => 1.25m,
|
|
BlastSubstrateType.Paint => 1.00m,
|
|
BlastSubstrateType.Mixed => 0.90m,
|
|
BlastSubstrateType.RustAndScale => 0.70m,
|
|
_ => 0.90m
|
|
};
|
|
}
|