Skip to main content
Technical Manual

Estimating Engine

The estimating engine turns quantities and catalog values into costs and a selling price. It is plain .NET code with no Orchard Core dependency, no I/O and no AI: the same input always gives the same result. Every amount uses decimal.

PackageNamespaceWhat it provides
CrestApps.Estimator.UnitsCrestApps.Estimator.UnitsUnits of measure, exact conversions, quantities, quantity bases (E, C, M), waste and purchase rounding
CrestApps.Estimator.EngineCrestApps.Estimator.Engine.*Labor, assembly expansion, line extensions, the recap, the estimate calculator and calculation traces
CrestApps.Estimator.DrawingCrestApps.Estimator.Drawing.*Drawing sheets: see Drawing Analysis

The engine does not read the catalog itself. The caller passes a snapshot of the items and assemblies, so later catalog changes never change a calculated revision.

Units​

BuiltInUnits holds the units every estimate can use. Conversion factors are exact definitions, so conversions between imperial and metric units round-trip without loss.

CodeNameDimensionBase unit
EAEachCounteach
LSLump sumLump sumlump sum
M, FT, LF, INMeter, foot, linear foot, inchLengthmeter
M2, SF, SYSquare meter, square foot, square yardAreasquare meter
M3, CF, CYCubic meter, cubic foot, cubic yardVolumecubic meter
KG, LB, TONKilogram, pound, short tonMasskilogram
HRHourTimehour

Quantity is a value and a unit. ConvertTo, + and - convert between units of the same dimension and throw InvalidOperationException for different dimensions.

UnitRegistry.CreateDefault() returns an IUnitRegistry with the built-in units. Industry packages register their own units with Register, for example the roofing square:

registry.Register(new UnitOfMeasure("SQ", "Roofing square", UnitDimension.Area, 9.290304m));

Quantity bases​

Prices and labor units can be quoted per one (E), per hundred (C) or per thousand (M) units, as electrical wire and conduit are. QuantityBasis.Extend(quantity, rate) divides by the basis, and QuantityBasisExtensions.TryParse reads the E, C and M codes.

Waste and purchase rounding​

PurchaseRounding.ApplyWaste adds a waste allowance and PurchaseRounding.RoundUpToPackage rounds up to whole packages, such as 500-foot reels. Both apply to material only: labor is always calculated from the installed quantity.

Labor​

LaborCalculator.Calculate returns base hours, the combined factor, adjusted hours and the cost, rounded to cents:

adjusted hours = quantity × labor unit hours ÷ basis × Π factors
cost = adjusted hours × hourly rate

A LaborFactor has a name, a value greater than zero and a LaborFactorScope. Factors apply in scope order: Estimate, Breakdown, Category, Line. A Line factor must state its reason.

CrewRate.Composite calculates the count-weighted average wage of a crew mix (CrewMember) plus burden.

Rates and burden​

LaborRateCalculator.LoadedRate(wage, burdens) adds each LaborBurden to a wage: a PercentOfWage burden adds that percentage of the wage, an AmountPerHour burden adds its amount. LaborRateCalculator.WithOvertime(rate, sharePercent, multiplier) blends a rate with overtime: rate × (1 + share / 100 × (multiplier − 1)), so 10% of the hours at 1.5 raises the rate by 5%.

Production mode​

ProductionCalculator.Calculate(quantity, unitsPerCrewHour, crew, factors, equipment) is the second way to consume labor, for work estimated by crew production rather than labor units per item. Crew hours are quantity ÷ unitsPerCrewHour × the factor stack; each CrewMember works the crew hours times its count, at its loaded rate; each EquipmentResource runs the crew hours. The ProductionResult gives the crew hours, the combined factor, the labor hours and cost, the equipment cost, the total and the hours and cost of each classification. The Estimator modules do not use production mode yet; it is in the engine so that heavy civil packages need no engine change.

Assemblies​

An AssemblyDefinition is a reusable bill of items with parameters and their defaults. Each AssemblyComponent is an item or a nested assembly, created with:

FactoryQuantity
AssemblyComponent.Item(code, perUnit)Assembly quantity × perUnit
AssemblyComponent.ItemByFormula(code, formula)The value of formula
AssemblyComponent.Assembly(code, perUnit)Assembly quantity × perUnit, then expanded
AssemblyComponent.AssemblyByFormula(code, formula)The value of formula, then expanded

Set Condition on a component to include it only when the condition formula is not zero, and WasteRate (a fraction, such as 0.1) to replace the waste of the item for this component.

AssemblyExpander.Expand(code, quantity, parameters) returns an ExpandedComponent per item, with its quantity, its assembly path (for example REC-2G/REC-20A/REC-DUP-20A) and the WasteRate of its component. ExpandTree returns the same items and also every assembly of the tree with its quantity and path (ExpandedAssembly), for assemblies that carry their own labor. Formulas read Quantity and the assembly parameters; values passed in parameters replace the defaults, and reach nested assemblies too. The expander throws InvalidOperationException for a cycle of assemblies or a negative formula result, and KeyNotFoundException for an undefined assembly.

The engine does not parse formulas. The caller supplies an IFormulaEvaluator; the Estimator Catalog module supplies one on the Orchard Core expression language (see Estimator Catalog module).

Lines​

ItemSnapshot holds the frozen catalog values of an item: unit, price and price basis, waste rate, package size, labor unit and labor category. LineCalculator.Calculate extends an installed quantity:

  1. Purchase quantity = installed quantity plus waste, rounded up to packages.
  2. Material = purchase quantity × price ÷ price basis, rounded to cents.
  3. Labor from the installed quantity, through LaborCalculator.

Each LineResult carries a Trace: one TraceEntry per step, with the step name, how it was computed and the value. This is what lets an estimate explain any number.

Recap​

RecapCalculator.Calculate(costs, settings, laborHours) runs the markup waterfall over CostTotals (material, labor, equipment, subcontracts, quotes, direct job expenses and allowances):

StepBasis
Material taxMaterial × MaterialTaxRate
Prime costEvery direct cost plus material tax
OverheadPrime cost × OverheadRate, plus labor hours × OverheadPerLaborHour
ProfitMarkup: cost × rate. Margin: cost ÷ (1 − rate) − cost. Cost is prime cost plus overhead. With CostTypeMarkups (markup only), each cost type × its rate (material with its tax), plus the overhead × ProfitRate
ContingencyCost plus profit × ContingencyRate
BondA BondSchedule of tiers, solved on the contract amount including the bond
RoundingThe selling price is rounded up to RoundTo

Every amount is rounded to cents (Money.Round, midpoints away from zero). The prime cost plus every adjustment always equals the selling price.

RecapCalculator.SolveProfitRate(costs, settings, targetPrice, laborHours) solves the profit rate that reaches a target selling price, with the bond on the price. It solves one rate on the whole cost (no cost type markups) without rounding, by bisection on the rate (a markup grows until it passes the target; a margin stays below 1), and keeps the rate whose price is nearest the target. A PriceToTargetResult gives the rate, the settings with it, the recap and whether the target is reachable: a target below the price without profit gives a rate of 0 and Reachable false.

Estimates​

EstimateCalculator ties the steps together. Create it with the item snapshots and an AssemblyExpander, then call Calculate(EstimateInput). Each EstimateLineInput names exactly one item or assembly. Estimate factors are added to each line's own factors. The result holds every line with its component extensions, the cost totals (lines plus DirectCosts), the labor hours and the recap.

Tests​

The engine has its own test project, tests/CrestApps.Estimator.Engine.Tests:

  • Unit tests for every calculation.
  • Property tests (FsCheck) for invariants, such as exact unit round-trips and the recap adding up to the selling price.
  • Golden estimates: each Golden/Fixtures/*.input.json estimate is recalculated and compared with its .approved.json snapshot. A difference fails the test and writes a .received.json file. Review it, and replace the approved file only when the change is intended.

Pull request CI runs the engine tests under dotnet-coverage with tests/engine.coverage.xml and keeps the Cobertura report as the engine-coverage artifact.