"""Curated Temple estimating model. Run temple_report.py; quantities are not automated design."""
from math import ceil,hypot
from collections import defaultdict
from pathlib import Path
import json,csv,hashlib
from decimal import Decimal, ROUND_HALF_UP
def round(value, ndigits=0):
 return float(Decimal(str(value)).quantize(Decimal(1).scaleb(-ndigits), rounding=ROUND_HALF_UP))
OUT=Path('work/temple_bom'); DATA=OUT/'data'
rows=[]; routes=[]; fixtures=[]; ducts=[]; circuits=[]; devices=[]; measures=[]; openings=[]; rfis=[]; prices=[]
RATES={'Site':48,'Concrete':52,'Masonry':55,'Framing':55,'Roofing':55,'Openings':58,'Finishes':48,'Casework':55,'Plumbing':68,'Mechanical':68,'Electrical':68,'Appliances':50,'General':60}
def add(trade,assembly,code,desc,q,u,p,h=0,source='',basis='Provisional',calc='',waste=0,pack=1,equip=0,price='EST',risk=.25):
 r=dict(id=f'B{len(rows)+1:04}',trade=trade,assembly=assembly,item_code=code,description=desc,net_qty=round(q,3),unit=u,waste=waste,pack=pack,material_rate=p,labor_hours_per_unit=h,labor_rate=RATES[trade],equipment_services=equip,source=source,quantity_basis=basis,calculation=calc or f'{q:g} {u} estimating allowance',price_source=price,risk=risk)
 rows.append(r);return r

def issue(title,basis,decision,action,priority='High'):
 rfis.append(dict(id=f'RFI-{len(rfis)+1:02}',priority=priority,topic=title,evidence=basis,estimate_treatment=decision,release_requirement=action))
def measure(id,description,value,unit,formula,source,basis):
 measures.append(dict(id=id,description=description,value=round(value,3),unit=unit,formula=formula,source=source,basis=basis));return value

def price(id,title,url,observed,adopted,note):
 prices.append(dict(id=id,title=title,url=url,checked='2026-10-02',observed=observed,adopted=adopted,note=note))
price('EST','Estimator budget rate','', 'No supplier quote','Per BOM row','All labor, custom fabrication and unreferenced material rates are judgment allowances for Sarasota; no measured local labor/productivity study.')
price('P01','Home Depot 4-inch PVC DWV 10-foot stick','https://www.homedepot.com/p/100348477','$77.13 / 10 ft',7.713,'Public retail anchor, not Sarasota quote; other diameters estimated.')
price('P02','SupplyHouse Spears CTS CPVC fittings','https://www.supplyhouse.com/CTS-CPVC-Fittings-18780000','$0.52 1/2 elbow; $0.87 reducing bushing; $1.85 female adapter','See rows','CTS benchmark only. NOT approval to substitute CTS for scheduled Sch40 IPS.')
price('P03','Cox Hardware CPVC 10-foot pipe','https://www.coxhardware.com/p-12176-cpvc-schedule-40-pipe-10-foot-sizes.aspx','$7.79 / $10.99 / $19.99 per 10 ft for 1/2, 3/4, 1 inch','0.80 / 1.10 / 2.00 per ft','Page title says Sch40 but description says SDR11/ASTM D2846. Base estimating family is CTS SDR11, pending RFI; not a matched Sch40 quote.')
price('P04','SupplyHouse AO Smith HNT-55','https://www.supplyhouse.com/ProLine-Master-Water-Heaters-25290000','$1,331.12',1400,'55-gallon 4.5kW analogous model; ENT-55 is scheduled, quote and nameplate needed.')
price('P05','National Supply Network HOM120PDFC','https://www.nationalsupplynetwork.com/HOM120PDFC-Square-D-Homeline-Plug-On-Neutral-Dual-Function-Breaker','$95.79',96,'Benchmark single-pole 20A dual-function; selected panel/breaker family to be coordinated.')
price('P06','Home Depot 8x8x16 concrete block','https://www.homedepot.com/p/100350252','$1.98, displayed Des Moines location',2.65,'Adjusted Sarasota budget, not local stock quote.')
price('P07','Cooling Direct 4-ton R454B equipment','https://cooling.direct/collections/all/4-ton?sort_by=price-ascending','$4,630 Airtemp 16 SEER2 heat pump; $2,270 B65 air handler','HP 5200; AHU 2600','Analogous equipment price benchmark, not a Trane matched-system quote.')
price('T01','Scheduled Trane A5HL5048A1000A','https://hvacdirect.com/4-ton-15-2-seer2-trane-inverter-heat-pump-condenser-r454b-inverter-rt-series-174707.html','Product identifies 15.2 SEER2; price not exposed','No price taken','Technical cross-check; AHRI matched combination still required.')
price('T02','Loren Cook Gemini','https://lorencook.com/product/gemini/','GC-100 family','No price taken','Fan budget $350 each; verify 50 CFM at scheduled 0.70 in.w.g. before selection.')
price('T03','Southwire 12/2 250ft','https://www.homedepot.com/p/Southwire-250-ft-12-2-Solid-Romex-SIMpull-CU-NM-B-W-G-Wire-28828269/202019375','Price not exposed',.80,'Product/package verification only; adopted wire rate remains EST.')

issue('Plumbing fixtures do not reconcile','A102 versus PP101/PP401/PP601: master double lavatory absent from P plan; porch sink tagged LV-02; schedule lists 2 KS but only one clearly routed.','Use A102: 5 bath lavatories + 2 kitchen/porch sinks + 1 laundry sink. Add omitted branches explicitly.','Issue coordinated fixture/rough-in schedule before slab.')
issue('Showers, hose bibbs and appliances','PP601 lists 5 SH-01; A102 shows 3 shower enclosures and one tub/shower. P plan has 3 HB; A102 has 5. A102 shows 2 DW and 2 REF.','Carry 5 valve/head sets: 2 master, 1 each guest shower, 1 tub/shower; 3 shower drains; 5 HB; 2 dishwasher connections; 2 ice maker boxes.','Confirm actual shower outlets and appliance list; delete optional second master set if not required.')
issue('Domestic water sizes and material family','PG001 material block calls CPVC Sch40 above grade and Type K below; generic notes also mention Type L. PP101/401 calls 2-inch DCW/DHW and 1-1/2 branches.','Retain large drawn diameters. Budget compatible CTS SDR11 water distribution and matching fittings as an estimating substitution ONLY; Type K service.','Engineer confirms hydraulic sizing, hot-water wait time, material/IPS-versus-CTS selection and compatible transitions; repricing required if Sch40 IPS retained.')
issue('Vent sizing and missing fixture branches','PP101/401 includes 2-1/2-inch vent labels; guest showers and architectural master lavs lack complete coordinated routing.','Carry drawn 2-1/2 vent mains with 2-inch branch vents; provisional lengths and fittings, no automatic reduction.','Approve coordinated DWV/wet vent layout, roof terminations, cleanouts and site invert.')
issue('Air handler schedule empty','MH601 AHU table has headings but no equipment row. MH101 shows one AHU-01. A102 shows two A/C closets.','One 4-ton-class matched AHU, one HP-1; 5kW supplemental heater allowance from A105 legend.','Engineer supplies AHU, heat kit, MCA/MOCP, clearances and matched AHRI data. No second system included.')
issue('Heat pump performance inconsistency','MH601 A5HL5048A1000A states 12.15 SEER2; vendor identifies same model as 15.2 SEER2; A110 says minimum 16 SEER.','Budget equipment above generic entry-level pricing; not acceptance of scheduled efficiency.','Confirm energy compliance, matched-system efficiency and equipment model.')
issue('Airflow schedule / device classification conflict','MG101 includes a stale Not Placed suite3 row and excludes observed Suite3/porch flows. MH101/401 also has unclassified D-01 at150CFM.','Retain15 supplies1530CFM and9returns1410CFM plus5exhaustfans250CFM. D-01 carriedseparately asunclassifiedequipment, not countedassupply. MG101 totals1310SA/1275RA differ.','Reconcile room names, all device tags/flows, load calculation, Manual D, outdoor air and pressure balance.')
issue('Water heater electrical and capacity','PP601 ENT-55: 4.5kW nominal, 6kW max, MCA31.25, MOCP50; note says dual4500W each max6kW; recovery20.7 labeled GPM.','55-gallon heater budget; reserved #8 copper / 40A two-pole estimating circuit only; RFI hold.','Verify exact heater nameplate, simultaneous/non-simultaneous elements, branch circuit, capacity and recovery units. Do not buy breaker from this allowance.')
issue('Electrical design incomplete','A105 has devices but no circuit numbers, panel schedule, load calculation, service size or utility detail.','Proposed circuit register, 200A service plus 100A subpanel budget; panel-space checks only. Copper throughout.','Licensed electrical designer finalizes load/service sizing, fault rating, protection, wire methods, derating, voltage drop and utility arrangement.')
issue('Roof assembly, drainage and parapet geometry','A106/107 TPO over #30 felt/5/8 plywood; slopes but no tapered build-up or manufacturer assembly; A109 shows 7 primary scupper positions, typical overflow.','3912 SF roof, 7 primary +7 separate overflow scuppers, 7 leaders; separate tapered insulation allowance and membrane upstands.','Provide approved roof assembly/uplift fastening, slopes, tapered takeoff, parapet heights, scupper sizes and site discharge design.')
issue('Slab and area reconciliation','A101 2867+255+675=3797 SF vs3125 AC and3912 covered; A1044-inch4000psi fiber vs A1103000psi mesh.','3797 SF slab plus24 SF stoop; 3122 finish SF;3912 roof.4000psi slab, both mesh and fiber held in base pending clarification.','Architect confirms area schedule and slab reinforcement; deduct unused reinforcement after resolution.')
issue('Structural systems and fabrication','A106/107 mixes CMU and18ga5.5-inch steel studs; no steel-truss shop layout or bar bending schedule.','Hybrid budget:8-foot CMU base plus4-foot exterior stud band and3-foot parapet; 24-inch truss spacing equivalent. Quantities provisional.','Engineer confirms wall-by-wall construction and heights, girder/truss schedule, connectors, reinforcement and large-opening headers. Do not fabricate from this estimate.')
issue('Ground/civil scope not supplied','Referenced civil continuation and2013 soils report absent. A109 says1500psf soil; A1102000psf.','70 LF water and70 LF sewer service allowances;6-inch fill only; site grading and storm components provisional.','Obtain current survey/geotech/civil utility inverts, lengths, fees, imported fill and drainage design.')
issue('Missing plumbing specification sheet','PG001 index lists8 entries including duplicated PP601; supplied set has7 pages endingPP802.','Read all7 received; no assumption missing specification page was reviewed.','Confirm full current P set and whether duplicate PP601 is an indexing error.')
issue('Window/door orders','A102 schedule codes1680 used for both16-foot SGD and1-foot6 swing; D14 2380 implies23-foot bifold; RO/height unverified.','Separate door mark register; impact exterior allowance; no product selections.','Approve opening shop drawings, elevations, egress, glass pressure ratings and hardware.')
issue('Condensate and acoustic screen','MP101 sanitary/soakaway vs A110 discharge2feet clear; A102 framed sound fence vs A110 masonry acoustic wall.','60ft total condensate/pan piping and soakaway allowance;16LF framed acoustic screen.','Coordinate approved discharge, traps/check-valve note, maintenance and condenser airflow/sound-screen detail.')
issue('Kitchen exhaust / dryer / make-up air','No coordinated hood or dryer routes on MH101 despite appliances onA102.','Two electric cooking stations, two hoods, one dryer; explicit rigid exhaust paths; one makeup-air provision.','Confirm hood CFM, outdoor discharge clearances and whether makeup air/interlock needed.')
issue('Finish and energy selections absent','No detailed finish schedule, cabinet elevations or energy report.','Midgrade owner-selection allowances; spray foam quantity provision, not assigned approved R-value.','Approve finishes, cabinet shop drawings, insulation/thermal barrier assembly and energy model.')
issue('Pool and outdoor utility interfaces','A102 pool BY OTHERS; no pool power, heater, pad or bonding design.','Pool/screen/deck excluded; no fictitious pool circuit. Water/landscape power interfaces listed.','Pool vendor supplies equipment loads, bond grid and interface scope. Price as separate package.')

# Architectural measurements and shells
AC=measure('Q01','Conditioned finish area',2867+255,'SF','2867 main +255 enclosed porch; cover total3125 differs3SF','A101','Scheduled / conflict')
SLAB=measure('Q02','Slab footprint',AC+675,'SF','Q01+675 garage; covered area not used as slab','A101/A104','Provisional reconciliation')
ROOF=measure('Q03','Roof horizontal area',3912,'SF','A101 total covered area used pending roof polygon/shop schedule','A101/A109','Provisional')
PER=measure('Q04','Exterior wall perimeter',278,'LF','2*(62ft11in+70ft)+12.17 entry/garage returns, rounded','A102B','Measured approximation')
INT=measure('Q05','Interior partitions gross centerline',392,'LF','West rooms/closets145 +east bath/closets112 +pantry/laundry65 +garage/entry70; openings not deducted for framing','A102B','Measured approximation')
BFT=measure('Q06','Interior bearing footing length',110,'LF','West bearing42 +central42 +east26; review truss bearing','A104','Measured approximation')
WIN=measure('Q07','Window opening area',288.5,'SF','18 scheduled windows; see opening register exact area supersedes this initial crosscheck','A102','Counted/derived')
# openings by mark / type
wtypes=[('W1',1,5,2),('W2',4,3,6),('W3',2,6,2),('W4',4,3,3),('W5',1,3+1/12,5),('W6',2,8,2),('W7',1,1+10/12,9),('W8',1,4,4),('W9',2,11+2/12,2)]
for tag,n,w,h in wtypes:
 openings.append(dict(mark=tag,count=n,width_ft=w,height_ft=h,type='Impact window',source='A102',status='Counted tags / scheduled type; verify RO'))
WIN=sum(x['count']*x['width_ft']*x['height_ft'] for x in openings);measures[-1]['value']=round(WIN,3)
doors=[('D1',3,9,'Exterior',2200),('D2',6,9,'Interior double glass',2100),('D3',2+8/12,9,'Interior swing',550),('D4',2+8/12,9,'Interior swing',550),('D5',2+8/12,9,'Interior swing',550),('D6',5,8,'Closet',650),('D7',2+8/12,9,'Interior swing',550),('D8',4,8,'Closet',500),('D9',4,8,'Closet',500),('D10',2+8/12,9,'Interior swing',550),('D11',2+8/12,9,'Exterior',1600),('D12',2+8/12,9,'Interior swing',550),('D13',16,8,'Impact slider',11500),('D14',23,8,'Impact bifold',24000),('D15',6,8,'Impact slider',3200),('D16',6,8,'Impact slider',3200),('D17',2+8/12,9,'Interior swing',550),('D18',2+8/12,8,'Pocket',430),('D19',2+8/12,8,'Pocket',430),('D20',2+8/12,8,'Cabinet swing',650),('D22',2+8/12,8,'Rated garage',800),('D23',2+8/12,8,'Interior swing',420),('D24',2+8/12,8,'Exterior',1500),('D25',4,8,'Closet',500),('D26',9,8,'Garage overhead',2000),('D27',16,8,'Garage overhead',3200),('D29',2,8,'Interior swing',400),('D30',4,8,'Closet',500),('D31',2,8,'Interior swing',400),('D32',2+8/12,8,'Interior swing',420),('D33',2+8/12,9,'Pocket',550),('D34',1.5,8,'Interior swing',380)]
for tag,w,h,typ,p in doors:openings.append(dict(mark=tag,count=1,width_ft=w,height_ft=h,type=typ,source='A102/A102B',status='Size code interpreted; verify RO'))
EXTDOOR=sum(w*h for _,w,h,t,_ in doors if t in ['Exterior','Impact slider','Impact bifold','Garage overhead'])
CMUAREA=PER*8-EXTDOOR-WIN*.65
STUDAREA=PER*4-WIN*.35
measure('Q08','Net CMU wall area',CMUAREA,'SF','278*8-exterior door area-65% window area;8ft CMU assumed','A106/A107','Provisional')
measure('Q09','Net upper exterior stud band',STUDAREA,'SF','278*4-35% window area;4ft band assumed','A106/A107','Provisional')
# Site explicit operations
for code,desc,q,u,p,h,eq,calc in [
('SILT','Silt fence installed',300,'LF',1.3,.04,0,'3 sides x100ft'),('TREEPRO','Tree protection fencing',100,'LF',1.4,.04,0,'100ft allowance'),('STAKE','Survey construction layout',1,'service',0,0,2200,'One layout survey service'),('CLEAR','Clear/grub footprint and staging',5500,'SF',0,.005,1100,'3797 slab plus1703 access/staging'),('TOPSOIL','Strip/store topsoil',51,'CY',0,.12,650,'5500SF*3in/27'),('FILL','Clean granular compacted fill delivered',SLAB*.5/27*1.2,'CY',38,.10,0,'3797SF*6in/27*1.20 loose conversion'),('COMPACT','Compact fill / plate roller rental',SLAB,'SF',0,.012,850,'3797SF compaction'),('TESTSOIL','Density/bearing testing visits',3,'visit',0,0,900,'3 tests/visits allowance'),('TERMITE','Pretreat soil treatment',SLAB,'SF',.32,.004,0,'Full slab footprint; retail chemical equivalent plus internal hours'),('DRIVE','Driveway pavers',1500,'SF',4.5,.055,0,'Site allowance pending dimensions; excludes pool deck'),('BASE','Paver base aggregate',1500*.5/27*1.15,'CY',48,.15,0,'1500SF*6in*1.15 loose'),('BED','Paver bedding sand',1500/12/27*1.15,'CY',55,.15,0,'1500SF*1in*1.15 loose'),('EDGE','Paver edge restraint',180,'LF',2.2,.04,0,'Drive/walk perimeter provision'),('PSAND','Polymeric sand',16,'bag',32,.18,0,'1500SF/100SF per bag rounded +one'),('SOD','Sod and preparation',4000,'SF',.65,.012,0,'Lot residual allowance'),('TREE','New canopy tree',1,'EA',550,4,0,'A101 one12ft/3in caliper'),('IRR','Irrigation poly lateral',400,'LF',.45,.025,0,'4zones*100ft'),('IRRHD','Irrigation heads',24,'EA',18,.3,0,'6heads*4zones'),('IRRVAL','Irrigation zone valves',4,'EA',45,.5,0,'4zones'),('IRRCTL','Irrigation controller',1,'EA',220,2,0,'1controller'),('IRRBACK','Irrigation backflow assembly',1,'EA',450,2,0,'Separate irrigation protection allowance'),('IRRWIRE','Irrigation control cable',250,'LF',.32,.01,0,'One250ft coil'),('LANDSOIL','Landscape soil / mulch',12,'CY',65,.5,0,'12CY allowance')]:
 add('Site','Site / landscape',code,desc,q,u,p,h,'A101',calc=calc,equip=eq,risk=.4)
# Concrete/masonry
for code,desc,q,u,p,h,w,pk,eq,calc in [
('EXC','Footing excavation', (PER+BFT)*2*1.5/27,'CY',0,.25,0,1,1500,'388LF*2ft*1.5ft/27'),
('FCONC','Footing concrete 3000psi', (PER+BFT)*2*(10/12)/27,'CY',195,.75,.07,.25,0,'(278+110)*2*10/12/27; no slab overlap'),
('SCONC','Slab concrete 4000psi', (SLAB+24)/81,'CY',205,1.3,.07,.25,0,'(3797+24)*4/12/27'),
('FIBER','Slab synthetic fiber admixture', (SLAB+24)/81,'CY',12,0,.07,.25,0,'Per slab concrete volume'),
('MESH','6x6 W1.4 welded mesh',SLAB,'SF',.35,.008,.10,750,0,'A110 held with fiber pending RFI'),
('VAPOR','6mil vapor retarder',SLAB,'SF',.065,.007,.15,2000,0,'6in laps and upturns allowance'),
('VAPTAPE','Vapor seam tape',720,'LF',.08,.003,.1,180,0,'6 rolls-equivalent minimum; seam estimate'),
('FORM','2x10 footing/slab form lumber',PER*2,'LF',1.7,.045,.05,16,0,'Two perimeter form faces'),
('STAKES','Form stakes reusable allowance',180,'EA',2.2,.02,0,1,0,'3ft spacing equivalent'),
('REBAR5','Footing #5 Grade60 bar',PER*3+BFT*2,'LF',.95,.025,.15,20,0,'F1 3bars; F3 2bars pluslaps/cuts'),
('CHAIR','Rebar chairs',ceil((PER+BFT)/4)*3,'EA',.8,.015,0,100,0,'3 rows at4ft'),
('TIEWIRE','16ga annealed tie wire',40,'LB',2.5,.03,0,4,0,'40lb provision'),
('CURE','Concrete curing compound',20,'GAL',18,.1,0,5,0,'3821SF/200SFpergal'),
('JOINT','Slab sawcut control joints',300,'LF',.08,.035,0,1,180,'Approx15ft grid; coordinate finishes'),
('EXPJOINT','1/2inch isolation joint',80,'LF',1.4,.035,0,10,0,'Stoop/garage transitions'),
('PUMP','Concrete pump mobilizations',2,'day',0,0,0,1,2600,'Footings plus slab'),
('TESTCON','Concrete cylinder sets',3,'service',0,0,0,1,600,'Footing slab grout visits')]:
 add('Concrete','Foundations',code,desc,q,u,p,h,'A104/A110',calc=calc,waste=w,pack=pk,equip=eq)
# stemwall three courses; above-wall split explicit avoids double count bondbeam
stem=PER*2*1.125; bond=PER*.75; blocks=CMUAREA*1.125-bond
for code,desc,q,u,p,h,w,pk,calc in [
('CMU8','8x8x16 standard CMU, stemwall',stem,'EA',2.65,.10,.05,1,'278LF*2ft*1.125blocks/SF'),
('CMU8','8x8x16 standard CMU, walls',blocks,'EA',2.65,.115,.05,1,'Net8ft wallarea*1.125 less one bondbeam course'),
('BOND8','8inch bond beam units',bond,'EA',4.6,.14,.05,1,'278LF/1.333ft'),
('MORTAR','TypeS mortar80lb bags',ceil((stem+blocks+bond)/12),'bag',10,.07,.05,1,'12 blocks/bag estimating yield'),
('GROUT','3000psi fine masonry grout',16,'CY',215,1.5,.08,.25,'Full stemwall plus112 reinforced cells and bondbeam provision'),
('VERT5','Vertical #5 reinforcing',112*12,'LF',.95,.025,.1,20,'112 reinforced positions*12ft includinghooks/laps;44inOC plusjambs'),
('BOND5','Bond beam2#5 continuous',PER*2,'LF',.95,.025,.15,20,'2bars*278LF'),
('LINTEL','Opening lintel concrete/rebar fabrication provision',165,'LF',28,.35,0,1,'Exterior door/window spans plusbearing; engineering hold'),
('JAMB','Masonry jamb straps/anchors',120,'EA',4.5,.08,0,1,'Opening andwall restraint allowance'),
('WATERPROOF','Stemwall waterproof coating',PER*2,'SF',.65,.018,.05,1,'278LF*2ft exterior'),
('CLEANOUT','Masonry cell cleanout/patch work',112,'EA',1.5,.14,0,1,'One per reinforced cell')]:
 add('Masonry','Masonry shell',code,desc,q,u,p,h,'A104/A106/A110',calc=calc,waste=w,pack=pk,price='P06' if code=='CMU8' else 'EST')
add('Masonry','Masonry shell','SCAFFOLD','Scaffold rental / delivery',1,'rental',0,24,'A106',equip=2400,calc='8 weeks scaffold budget; erection hours separate')
# framing, roofs
for code,desc,q,u,p,h,w,pk,calc in [
('TRUSS','Engineered steel roof trusses fabricated',ROOF,'SF',7.25,.035,0,1,'3912SF roof framing package; approx35 bays at24in, fabricated members NOT resolved'),
('TRUSSENG','Truss engineering/shop drawings',1,'service',2200,0,0,1,'One engineered package'),
('ANCHOR','Engineered truss bearing anchors',90,'EA',20,.2,.05,1,'Approx45truss pieces*2bearings; actual layout missing'),
('BRACE','Steel truss bracing/bridging',800,'LF',1.8,.035,.1,10,'Shoplayout allowance, not member schedule'),
('ESTUD','18ga5.5inch exterior steel studs',ceil(PER/(16/12))*7+240,'LF',2.8,.035,.08,12,'4ft upperband+3ft parapet at16in +240LF corners/jambs'),
('ETRACK','18ga5.5inch top/bottom track',PER*4,'LF',2.3,.025,.08,10,'Band andparapet each2tracks'),
('ISTUD','Interior3-5/8inch steel studs',ceil(INT/(16/12))*12+600,'LF',1.35,.025,.08,12,'392LF at16in,12ft high plus600LFjambs/corners; gauge engineered forheight'),
('ITRACK','Interior3-5/8inch track',INT*2,'LF',1.15,.025,.08,10,'Top+bottom'),
('HEADER','Interior steel door headers',120,'LF',3.5,.08,.08,10,'Nonloadbearing header allowance'),
('BLOCK','PT2x6 blocking',420,'LF',1.4,.05,.1,12,'Opening treatments, cabinets andbathroom fittings'),
('STUDSCREW','Self-drilling framing screws',6500,'EA',.075,.001,0,1000,'Stud/track/bracing assembly provision'),
('TRACKANCH','Track concrete anchors',ceil((PER*2+INT)/2),'EA',.8,.025,.05,100,'2ft attachment estimating spacing; design hold'),
('SHEATH','1/2inch glass-mat exterior sheathing',STUDAREA+PER*3*2,'SF',1.45,.025,.1,32,'Upperband outside +bothparapetfaces'),
('SHEATHSC','Exterior sheathing screws',5000,'EA',.07,.001,0,1000,'6in nominal fastenerpattern allowance'),
('PLY','5/8inch plywood roof sheathing',ROOF,'SF',1.25,.018,.10,32,'A106specified plywood; sheets rounded'),
('HCLIP','Plywood H-clips',ceil(ROOF/32)*3,'EA',.42,.012,.05,250,'3clips per roof sheet estimate'),
('ROOFSC','Roof-to-steel approved screws',11000,'EA',.13,.001,0,1000,'Engineered field/edge/corner schedule pending'),
('FOAM','Spray polyurethane roof insulation',ROOF*7,'board-ft',.25,.0015,.05,1,'7inch open-cell budget only; R-value and roofassembly pending'),
('THERMBAR','Foam thermal/ignition barrier coating',ROOF,'SF',.45,.012,.05,1,'Conditional assembly provision'),
('WALLINS','Exterior wall insulation',PER*12-EXTDOOR-WIN,'SF',.95,.018,.05,1,'Net envelope walls provisional system'),
('SOUNDINS','Interior acoustic batts',INT*12*.6,'SF',.8,.012,.05,1,'60%partitions allowance')]:
 add('Framing','Shell / partitions',code,desc,q,u,p,h,'A104/A106/A107/A108/A110',calc=calc,waste=w,pack=pk,risk=.3)
add('Framing','Roof erection','CRANE','Crane and rigging rental',2,'day',0,0,'A108',equip=3200,calc='Two crane days; rigging labor in truss rate')
memarea=ROOF+PER*3
for code,desc,q,u,p,h,w,pk,calc in [
('FELT','No30 roof dry-in felt',ROOF,'SF',.18,.007,.1,216,'A106; final TPO compatibility unresolved'),
('TAPER','Tapered roof insulation slope provision',ROOF,'SF',2.1,.015,.05,32,'No tapered layout provided; separate from undersidefoam'),
('COVER','Roof cover board1/2inch',ROOF,'SF',1.15,.012,.05,32,'Provisional approved TPO assembly component'),
('TPO','White60mil TPO membrane',memarea,'SF',1.15,.026,.12,1000,'3912field+278LF*3ft upstands; laps/cuts12%'),
('ADH','TPO bonding adhesive',ceil(memarea/60),'GAL',30,.08,0,5,'60SF/GAL two-surface installedyield allowance'),
('RPLATE','Insulation plates/screws',2400,'EA',.85,.008,.05,100,'Uplift pattern placeholder; engineer/mfr approve'),
('TERM','Termination bar',PER,'LF',2.1,.035,.05,10,'Perimeter termination'),
('COPING','Aluminum parapet coping',PER,'LF',13,.08,.07,10,'Developedwidth/manufacturer pending'),
('CLEAT','Coping continuous cleat',PER*2,'LF',2.4,.035,.05,10,'Bothedges'),
('COPSPL','Coping splice plates',ceil(PER/10),'EA',9,.1,0,1,'Oneper10ft length'),
('CORNERS','TPO prefab corner flashings',24,'EA',16,.16,0,1,'12inside+12outside provisional'),
('SEALROOF','Roof sealant cartridges',40,'EA',12,.08,0,1,'Terminations/scuppers/coping'),
('SCUPPER','Primary TPO-clad scupper',7,'EA',210,1.2,0,1,'A1093west+4east'),
('OVERFLOW','Independent overflow scupper',7,'EA',180,1.2,0,1,'Oneper primary; actual separation/size pending'),
('LEADERHD','Collector head',7,'EA',130,.6,0,1,'Oneperprimary'),
('LEADER','4x4aluminum leader',7*15,'LF',7,.08,.05,10,'7leaders*15ft'),
('LEADEREL','Leader elbow',14,'EA',18,.12,0,1,'2perleader'),
('LEADERSTR','Leader strap',35,'EA',5,.08,0,1,'5perleader'),
('STORM','4inch solid storm pipe',200,'LF',4,.07,.1,10,'7drains connecting tosite discharge allowance'),
('STORM45','4inchstorm45elbow',14,'EA',12,.12,0,1,'2perleader tie-in'),
('STORMTEE','4inchstormwye',7,'EA',22,.18,0,1,'Oneperleader'),
('STORMEND','Drainage outlet/cleanout terminal',7,'EA',42,.4,0,1,'7dischargepoints; nocivil design'),
('SOFFIT','Stucco soffit substrate',160,'SF',2.1,.04,.08,32,'Frontoverhang provision')]:
 add('Roofing','Roof / storm',code,desc,q,u,p,h,'A106/A107/A109',calc=calc,waste=w,pack=pk,risk=.3)
add('Roofing','Roof / storm','WELDER','Hot-air welder rental',1,'rental',0,0,'A106',equip=550,calc='Oneweek rental')
# openings individual assemblies, accessories separately
for tag,n,w,h in wtypes:
 add('Openings',tag,'WIN-'+tag,f'Impact window {tag}, {w:g}x{h:g}ft',n,'EA',max(600,w*h*65),2+w*h*.06,'A102',basis='Counted / scheduled',calc=f'{n} tags; {w:g}x{h:g}ft; frame/glass factory assembly',risk=.35)
for tag,w,h,t,p in doors:
 add('Openings',tag,'DOOR-'+tag,f'{t} {w:g}x{h:g}ft, frame included',1,'EA',p,4+w*.7,'A102/A102B',basis='Counted / scheduled',calc=f'{tag}; confirm handing/size before order',risk=.35)
 if t=='Pocket':add('Openings',tag,'POCKETKIT','Pocket frame/track kit',1,'EA',230,2,'A102',basis='Derived',calc=f'{tag}1kit; leaf line excludes pocket frame')
 if t in ['Interior swing','Interior double glass','Cabinet swing','Exterior','Rated garage']:
  add('Openings',tag,'LOCK-'+('EXT' if t in ['Exterior','Rated garage'] else 'INT'),'Lever/lockset hardware',1,'EA',130 if t in ['Exterior','Rated garage'] else 45,.35,'A102',basis='Derived',calc=f'{tag}; hinges included withprehungdoor')
 if t=='Garage overhead':add('Openings',tag,'OPENER','Garage operator, rail, remotes and sensors',1,'EA',420,2.5,'A102/A105',basis='Derived',calc='Oneperoverhead door; operator power inElectrical')
for code,d,q,u,p,h,w,pk in [('FLASH','Window/door flashing tape',1100,'LF',.8,.025,.1,75),('SILLPAN','Exterior opening sill pans',26,'EA',55,.4,0,1),('OPENANCH','Approved frame anchors',800,'EA',.85,.018,.05,100),('OPENFOAM','Low-expansion window foam',25,'can',12,.1,0,1),('OPENSEAL','Opening sealant',65,'tube',9,.12,0,1),('BUCK','PTopening bucks',900,'LF',1.4,.04,.1,12)]:
 add('Openings','Opening installation',code,d,q,u,p,h,'A102/A110',calc='Opening perimeters/anchors allowance; includeslargeglazeddoors',waste=w,pack=pk)
# finishes and cabinetry explicit materials
wallboard=INT*12*2+PER*12-EXTDOOR-WIN-600
for code,d,q,u,p,h,w,pk,calc in [
('GYPW','1/2inch gypsum wallboard',wallboard,'SF',.48,.017,.10,48,'392LF*12ft*2 +netexteriorlining -600wetwallcementboard'),
('GYPC','1/2inch ceiling-rated gypsum',SLAB,'SF',.55,.021,.1,48,'All roomsandgarage ceiling'),
('CEMBOARD','1/2inch cement backerboard',600,'SF',1.35,.035,.1,15,'3showers+tubshower tilewalls'),
('GYPSCREW','Drywall screws',15000,'EA',.012,.0005,0,1000,'Board fastener allowance'),
('JOINTCOMP','Jointcompound4.5gal',38,'pail',25,.55,0,1,'Approx350SF/pail'),
('JOINTTAPE','Paper jointtape',4000,'LF',.018,.006,0,500,'Board seam estimate'),
('BEAD','Cornerbead',650,'LF',.7,.03,.05,10,'Corners andopeningreturns'),
('STUCCO','Stucco cement/sand dry mix80lb',ceil((PER*12-EXTDOOR-WIN+PER*3)/14),'bag',12.5,.23,.08,1,'14SF/bag blended5/8-7/8coverage'),
('WRB','Two-layer stucco weather barrier',STUDAREA+PER*3,'SF',.5,.012,.1,324,'Upperframedwall+outerparapet;2layers includedinrate'),
('LATH','Galvanizedmetal lath',STUDAREA+PER*3,'SF',.95,.025,.08,18,'Framedstucco substrate'),
('STUCCOBEAD','Stucco weep/corner/controlbead',800,'LF',1.4,.025,.05,10,'Panelization provision'),
('PAINTPR','Interior primer',ceil((wallboard+SLAB)/350),'GAL',26,.14,.05,5,'350SF/GAL onecoat'),
('PAINTIN','Interior finishpaint',ceil((wallboard+SLAB)*2/350),'GAL',40,.18,.05,5,'2coats350SF/GAL'),
('PAINTEX','Exteriorpaint/sealer',ceil((PER*15-EXTDOOR-WIN)*3/300),'GAL',42,.18,.05,5,'1primer+2finishcoats300SF/GAL'),
('FLOORTILE','Interior porcelain flooring',AC,'SF',4.5,.07,.12,16,'Fullconditionedfootprint budget includeswetfloors;fieldverifywall deductions'),
('THINSET','Floor thinset50lb',ceil(AC/50),'bag',28,.15,.05,1,'50SF/bag'),
('GROUTTILE','Floor grout25lb',ceil(AC/150),'bag',38,.2,.05,1,'150SF/bag'),
('LEVEL','Floorprep/leveler50lb',24,'bag',35,.3,0,1,'Localleveling provision'),
('WALLTILE','Shower/tub walltile',600,'SF',6,.10,.12,12,'600SFwetwalls'),
('WTHIN','Wallthinset50lb',12,'bag',32,.15,.05,1,'600SF/50'),
('WGROUT','Wallgrout25lb',6,'bag',38,.2,.05,1,'600SF/100'),
('WATERPROOF','HydroBan-equivalent wetarea membrane',700,'SF',1.5,.025,.1,1,'Walls600 +showerfloor100; coveragefor2coats'),
('PANMORTAR','Showerpan slope mortar60lb',25,'bag',9,.2,0,1,'3showerpans; tubareaexcluded'),
('SHOWERGLASS','Showerglass/enclosure fabricated',3,'EA',1450,3,0,1,'Master+suites2/3;no showerdoor over tubassumed'),
('BASE','Paintgrade baseboard',950,'LF',1.9,.06,.1,16,'Netwallrunsafterdoors/cabinetdeductions'),
('CASING','Interior door/window casing',1250,'LF',1.6,.05,.1,16,'Openingperimeter allowance'),
('TRIMNAIL','Finishnails',5000,'EA',.012,.001,0,1000,'Trim fastening'),
('CAULK','Paintable interiorcaulk',60,'tube',4,.12,0,1,'Trimandjoints'),
('BATHACC','Towelbar/TP/robe accessoryset',4,'set',110,.6,0,1,'4bathrooms'),
('MIRROR','Bathroom mirror',5,'EA',180,.5,0,1,'5bath lavatories'),
('EPOXY','Garagefloor epoxy system',675,'SF',1.8,.03,.05,1,'Ownerselection allowance')]:
 add('Finishes','Interior / exterior finishes',code,d,q,u,p,h,'A102/A106/A110',calc=calc,waste=w,pack=pk,risk=.3)
for loc,base,upper,tall in [('Kitchen',28,14,4),('Pantry',9,9,4),('Enclosed porch kitchen',10,0,0),('Laundry',8,8,0),('Master bath',8,0,0),('Guest baths',9,0,0)]:
 for typ,q,p in [('Base',base,260),('Upper',upper,200),('Tall',tall,420)]:
  if q:add('Casework',loc,'CAB-'+typ,typ+' cabinet carcass/doors/drawers/hardware factory assembly',q,'LF',p,.7,'A102',calc=f'{loc} scaled cabinet frontage allowance;shop elevationsabsent',risk=.35)
 if base:add('Casework',loc,'QUARTZ','Quartz top fabricated withcutouts',base*2.25,'SF',55,.12,'A102',calc=f'{base}LF*2.25ft nominaldepth;islandadditional below',risk=.35)
add('Casework','Kitchen island','QUARTZ','Island top extra depth beyond2.25ft',28,'SF',55,.12,'A102',calc='Approx10ftx5ft less22.5SF includedbasefrontage',risk=.35)
for code,d,q,u,p,h in [('CABFAST','Cabinetmountingscrews',300,'EA',.15,.012),('CABFILL','Cabinetfillers/endpanels',20,'EA',65,.35),('CLOSET','Closetshelving/rod',100,'LF',18,.2),('LINEN','Linen/pantryshelving',65,'LF',16,.18),('BENCH','Drop-offbench/backpanel',1,'EA',850,5)]:
 add('Casework','Millworkaccessories',code,d,q,u,p,h,'A102',calc='Separate installation/accessoryprovision')
# Plumbing fixture register: physical fixtures reconciled against architecture, not P schedule grand total.
# x,y feet east/south from west wall / garage front grid; schematic estimating coordinates.
for id,loc,typ,x,y,cw,hw,dia in [
('L1','Suite2 bath','Lavatory',8,34,1,1,'1.5'),('L2','Suite3 bath','Lavatory',8,53,1,1,'1.5'),('L3','Suite4 bath','Lavatory',20,65,1,1,'1.5'),('L4','Master bath north','Lavatory',52,35,1,1,'1.5'),('L5','Master bath south','Lavatory',52,42,1,1,'1.5'),
('K1','Kitchen island','Kitchen sink',40,41,1,1,'1.5'),('K2','Porch kitchen','Kitchen sink',47,63,1,1,'1.5'),('U1','Laundry','Laundry sink',54,29,1,1,'2'),
('W1','Suite2 bath','Toilet',5,34,1,0,'3'),('W2','Suite3 bath','Toilet',5,53,1,0,'3'),('W3','Suite4 bath','Toilet',20,68,1,0,'3'),('W4','Master bath','Toilet',54,46,1,0,'3'),
('S1','Suite2 bath','Shower',3,32,1,1,'2'),('S2','Suite3 bath','Shower',3,51,1,1,'2'),('S3','Master shower primary','Shower',59,33,1,1,'2'),('S4','Master shower secondary head','Shower second head',61,33,1,1,''),
('T1','Master bath','Freestanding tub',59,42,1,1,'1.5'),('T2','Suite4 bath','Tub shower',16,67,1,1,'1.5'),
('DW1','Kitchen island','Dishwasher',37,41,0,1,''),('DW2','Pantry','Dishwasher',49,27,0,1,'1.5'),('IM1','Kitchen refrigerator','Ice maker',34,35,1,0,''),('IM2','Pantry refrigerator','Ice maker',48,27,1,0,''),('WM1','Laundry','Washer box',61,29,1,1,'2'),
('H1','Northwest exterior','Hose bibb',0,20,1,0,''),('H2','Southwest exterior','Hose bibb',0,68,1,0,''),('H3','Garage east','Hose bibb',63,17,1,0,''),('H4','Master east','Hose bibb',63,68,1,0,''),('H5','Porch south','Hose bibb',49,70,1,0,'')]:
 fixtures.append(dict(id=id,location=loc,type=typ,x=x,y=y,cold=cw,hot=hw,waste_size=dia,source='A102; PP101/PP401/PP601 reconciliation',basis='Counted architecture; estimating connections',rfi='RFI-01 / RFI-02'))
 # Fixture body/trim and connection kits; no pipe lengths in kits (route register owns pipes).
 kit=[]
 if typ in ['Lavatory','Kitchen sink','Laundry sink']:
  kitchen=typ=='Kitchen sink'; laundry=typ=='Laundry sink'
  kit=[('BASIN-'+typ,typ+' basin; cabinet/top excluded',1,350 if kitchen else 190 if laundry else 150,1),('FAUCET-'+typ,typ+' faucet with manufacturer supply tails',1,280 if kitchen else 160,.65),('STOP','1/2 x3/8 quarter-turn angle stop',2,12,.18),('ESC','Supply escutcheon',2,2,.035),('TRAP','1-1/2 tubular P-trap kit',1,15,.35),('TRAPAD','DWV1-1/2 trap adapter',1,3,.12),('DRAIN','Basket strainer/tailpiece or pop-up drain',1,30,.3),('STUB','1/2 threaded transition/stub-out',2,7,.15)]
  if kitchen:
   kit += [('DISPOSER','3/4HP disposal withmountingflange',1,220,.65),('DWTAIL','Dishwasher branch / disposal connection',1,8,.1)]
 if typ=='Toilet':
  kit=[('WC','Elongated WaterSense toilet withseat',1,300,1.25),('FLANGE','3x4PVC closetflange stainlessring',1,24,.35),('WCSEAL','Closetseal',1,9,.15),('WCBOLT','Closet bolt/cap set',1,6,.08),('STOP','1/2 x3/8 quarter-turn angle stop',1,12,.18),('WCSUP','Braided closet supply3/8x7/8',1,9,.12),('ESC','Supply escutcheon',1,2,.035),('STUB','1/2 threaded transition/stub-out',1,7,.15)]
 if typ in ['Shower','Shower second head','Tub shower']:
  kit=[('SHVAL','Pressure-balance shower rough valve',1,130,.65),('SHTRIM','Shower trim/head/arm/flange set',1,180,.45),('SHDROP','Brass drop-ear elbow1/2',1,9,.18),('SHAD','CPVC-to-valve threaded transition',3,6,.12),('SHBRACE','Valve/head support bracket',2,5,.15)]
  if typ=='Shower':kit += [('SHDRAIN','2inch bonding shower drain',1,80,.5),('SHTRAP','2inchPVC solvent P-trap',1,12,.3)]
  if typ=='Tub shower':kit += [('TUB60','60x36 bathtub',1,650,1.8),('TUBWASTE','Tub waste/overflow kit',1,70,.65),('TUBTRAP','1-1/2PVC solvent P-trap',1,10,.3),('TUBSPOUT','Tub diverter spout',1,45,.2)]
 if typ=='Freestanding tub':
  kit=[('TUBFREE','Freestanding soaking tub',1,1400,2.5),('TUBFILL','Freestanding tub filler androughmount',1,850,1.5),('TUBWASTE','Tub waste/overflow kit',1,70,.65),('TUBTRAP','1-1/2PVC solvent P-trap',1,10,.3),('SHAD','CPVC-to-valve threaded transition',2,6,.12)]
 if typ=='Dishwasher':
  kit=[('DWSTOP','Dedicated dishwasher quarter-turn stop',1,15,.18),('DWSUP','Dishwasher braided supply/90adapter',1,22,.2),('DWHOSE','Dishwasher drain extension/highloophardware',1,22,.2),('HAMMER','Water hammer arrestor',1,22,.15)]
  if id=='DW2':kit += [('DWIND','Pantry dishwasher indirect waste receptor connection provision',1,65,.6)]
 if typ=='Ice maker':kit=[('ICEBOX','Ice maker outlet boxwithvalve/arrestor',1,48,.45),('ICESUP','Braided ice maker connector',1,18,.15)]
 if typ=='Washer box':kit=[('WMBOX','Washer outletbox valves andintegralarrestors',1,95,.65),('WMSUP','Washer braided hoses pair',1,35,.2),('WMTRAP','2inchPVC washer trap',1,12,.3),('WMPAN','Washer drainpan',1,45,.3)]
 if typ=='Hose bibb':kit=[('HB','3/4hosebibb withvacuum breaker',1,30,.25),('HBAD','Hosebibb drop-ear/transition',1,12,.2),('HBANCH','Hosebibb mounting/penetration kit',1,8,.2)]
 for code,d,n,p,h in kit:
  add('Plumbing',id+' '+loc,code,d,n,'EA',p,h,'A102/PP101/PP501/PP601',basis='Derived fixture kit',calc=f'{id}: {n} per fixture; supply tails included with faucet, no separate hoses',risk=.25)
# Core equipment connections
for code,d,q,u,p,h,calc in [
('WH55','55gallon electric storage heater, ENT-55 basis',1,'EA',1400,3,'One WH-1; analogousHNT55 pricing;nameplatehold'),('WHPAN','28inch waterheater pan',1,'EA',45,.3,'Oneperheater'),('WHEXP','Potable thermal expansiontank',1,'EA',70,.5,'Oneclosed-systemtank;engineer size'),('WHBR','Expansiontank supportbracket',1,'EA',22,.3,'One'),('WHCON','Listed waterheater flexibleconnector',2,'EA',28,.2,'Cold/hot connections'),('WHVAL','3/4ballvalve leadfree',2,'EA',22,.2,'Cold/hot isolation'),('WHTMV','Thermostatic mixingvalve assembly',1,'EA',170,.6,'Budgetdeliverytemperaturecontrol'),('WHREL','T&P valve check/installation labor, valve furnishedwithheater',1,'EA',0,.2,'Avoidduplicatevalvepurchase'),('WHSTR','Waterheater anchorage strapkit',1,'EA',28,.35,'Anchorageprovision'),('WHAUTO','Automaticwhole-house water shutoff / sensors',1,'EA',550,1.5,'A110 requirement;finalsize and bypass'),('PRV','2inch water pressure reducingvalve provision',1,'EA',425,.7,'Pressureunknown; includedbase pendingutility'),('BACKFLOW','2inch RPZ backflow assembly',1,'EA',950,1.5,'PG001 callsRPZ;utility confirmsactualneed/size'),('MAINV','2inch leadfree isolationballvalve',2,'EA',135,.3,'Serviceandbuildingentry'),('UNION2','2inch dielectric/transition unions',4,'EA',45,.25,'Serviceequipment'),('WTRBOX','Watermeter/valvebox',1,'EA',160,.7,'Meterutilitychargeexcluded'),('WTRTEST','Waterpressure/disinfection/testconsumables',1,'kit',120,8,'Onecompletewatersystem'),('DWVTEST','DWV testplugs/balls andtestwater',1,'kit',160,8,'CompleteDWV test'),('BACKTEST','Backflow certification',1,'service',0,0,'Independenttestfee inservices'),('PLSTART','Plumbing trimadjustment/commissioning',1,'activity',0,16,'Temperature setting/leakchecks/fixtureoperation'),('FIREP','Listed pipepenetration sealant',12,'tube',22,.12,'PG001/PP502;approvedassemblyselection'),('SLEEVE','Pipe wall/slab sleeve',48,'EA',4,.12,'27fixturepoints plusmains/roof'),('ACCESS','14x14accesspanel',10,'EA',35,.3,'Concealedvalve/cleanoutprovision'),('LABELP','Pipe/valve labels',60,'EA',1.2,.035,'PG001 tagging')]:
 r=add('Plumbing','Service / waterheating / closeout',code,d,q,u,p,h,'PG001/PP101/PP501/PP601/A110',calc=calc,price='P04' if code=='WH55' else 'EST')
 if code=='BACKTEST':r['equipment_services']=180
# routes. drawn paths scaled; not cut lengths; fitting takeout NOT deducted.
def route(id,system,size,start,end,points,rise=0,extra=0,status='Estimating route',source='PP101/PP401'):
 length=sum(hypot(b[0]-a[0],b[1]-a[1]) for a,b in zip(points,points[1:]));
 routes.append(dict(id=id,system=system,size=size,from_node=start,to_node=end,points=points,horizontal_ft=round(length,2),vertical_ft=rise,connection_ft=extra,net_ft=round(length+rise+extra,2),status=status,source=source))
route('CW00','TypeK water','2','Meter allowance','Building entry',[(63,-47),(63,23)],0,2,'Utility length allowance')
route('CW01','CPVC cold','2','Entry','WH tee',[(63,23),(38,23)],10,2)
route('CW02','CPVC cold','1.5','WH tee','West header',[(38,23),(10,23),(10,58)],0,2)
route('CW03','CPVC cold','1.5','Entry','East header',[(58,23),(51,23),(51,64)],0,2)
route('HW01','CPVC hot','2','WH','Hot header',[(38,23),(48,23)],10,2)
route('HW02','CPVC hot','1','WH','West hot header',[(38,23),(11,23),(11,58)],0,2)
route('HW03','CPVC hot','1.5','Hot header','East hot header',[(48,23),(50,23),(50,45)],0,2)
route('HW04','CPVC hot','0.75','East hot header','Porch/tub header',[(50,45),(50,65)],0,2)
for f in fixtures:
 x,y=f['x'],f['y']; west=x<26; hx=10 if west else 51; hy=min(max(y,23),58 if west else 64)
 for key,system in [('cold','CPVC cold'),('hot','CPVC hot')]:
  if not f[key]:continue
  sx=hx if key=='cold' else hx+1 if west else hx-1
  size='0.75' if f['type']=='Hose bibb' else '0.5'
  route(('C-' if key=='cold' else 'H-')+f['id'],system,size,'West header' if west else 'East header',f['id'],[(sx,hy),(x,hy),(x,y)],10,2,'Provisional fixture branch; high distribution/drop')
# hot/cold fixture connections and pipe fittings generated from topology
water_rates={'0.5':(.80,.025),'0.75':(1.10,.028),'1':(2,.035),'1.5':(4.8,.045),'2':(7.5,.055)}
for size,(p,h) in water_rates.items():
 rr=[r for r in routes if r['system'].startswith('CPVC') and r['size']==size];q=sum(r['net_ft'] for r in rr)
 if not q:continue
 add('Plumbing','Water distribution','CPVC'+size,f'CPVC CTS SDR11 potable pipe {size}inch; substitution hold',q,'LF',p,h,'PP101/PP401; route register',calc='Sum '+', '.join(r['id'] for r in rr),waste=.1,pack=10,price='P03' if size in ['0.5','0.75','1'] else 'EST',risk=.3)
 elbows=sum(max(0,len(r['points'])-2)+2 for r in rr)
 for typ,n,rate,hrs in [('90elbow',elbows,{'0.5':.52,'0.75':1,'1':2.64,'1.5':5.5,'2':10}[size],.12),('coupling',ceil(q/10),{'0.5':.4,'0.75':.7,'1':1.6,'1.5':3.4,'2':6}[size],.1),('testcap',max(2,len(rr)),{'0.5':.4,'0.75':.6,'1':1.3,'1.5':3,'2':5}[size],.05)]:
  add('Plumbing','Water distribution','C-'+size+'-'+typ,f'CPVC CTS {size}inch {typ}',n,'EA',rate,hrs,'Route topology / PG001',basis='Derived provisional fittings',calc='Route turns +2 riser bends each; couplings1per10ft;capsperroute; fittingtakeoutnotdeducted',waste=.05,price='P02' if size=='0.5' and typ=='90elbow' else 'EST')
 add('Plumbing','Water distribution','INS-'+size,f'Closed-cell water pipeinsulation {size}inch, provisional1/2wall',q,'LF',.75+float(size)*.6,.018,'PG001',calc='AllhotandcoldroutesperPG001;thicknessfinalenergycoordination',waste=.05,pack=6)
 add('Plumbing','Water distribution','HANG-'+size,f'Isolating pipeclamp {size}inch',ceil(q/3),'EA',1.5+float(size),.05,'PG001 / route register',calc=f'{q:.2f}LF /3ft estimating support spacing; verifymanufacturer',waste=.05)
# Reducing branch tees explicitly by receiving header; owner-selected fitting family toconfirm
for size in ['1','1.5']:
 n=sum(1 for r in routes if r['id'].startswith(('C-','H-')) and (('West' in r['from_node'] and r['system']=='CPVC hot') == (size=='1')))
 add('Plumbing','Water distribution','CTEE-'+size,f'CPVC CTS {size} x{size} x1/2 reducing tee / reducer assembly',n,'EA',5 if size=='1' else 8,.18,'Route branch register',calc='Oneeachfixturefeed; hosebranchesrequire3/4outletinplaceof1/2; noextrateecounted')
for code,d,q,u,p,h,pk in [('CTEE2','2inchCPVC maintee',3,'EA',15,.2,1),('CRED21','2x1-1/2CPVC reducer',4,'EA',8,.12,1),('CRED10','1-1/2x1CPVC reducer',2,'EA',5,.12,1),('CRED075','1-1/2x3/4CPVC reducer',2,'EA',5,.12,1),('CVAL','3/4or1inch branchisolationvalve',12,'EA',25,.2,1),('CSOLV','CPVC potable-rated cement',3,'quart',18,.1,1),('CPRIM','Compatible primer where required',2,'quart',12,.1,1),('CSUPSC','Pipe support screws/anchors',500,'EA',.12,.005,100),('CRELIEF','3/4listed T&P dischargepipe',25,'LF',1.4,.035,10),('CPAN','1inch PVCpan drain',25,'LF',.85,.03,10),('CPAND','Pan/dischargeroute fittings',10,'EA',2,.12,1),('KSERV','2inchTypeK copper water service',72,'LF',25,.06,20),('KTRANS','2inchTypeK serviceendtransition',2,'EA',75,.5,1),('WTRTRENCH','Water service trench/backfill',70,'LF',0,.16,1)]:
 r=add('Plumbing','Water service / distribution',code,d,q,u,p,h,'PG001/PP101/PP501',calc='Equipment/serviceorheaderconnectionprovision;routeCW00 owns72ftservice',pack=pk)
 if code=='WTRTRENCH':r['equipment_services']=400
# Sanitary routes including explicit missing connections. DWV tees/wyes separate from straight pipe.
route('D00','PVC sanitary','4','Street sewer allowance','Building exit',[(31,-70),(31,0)],0,0,'Utility length allowance')
route('D01','PVC sanitary','4','Exit','Main south',[(31,0),(31,60)],0,2)
route('D02','PVC sanitary','3','Main north','Suite2 group',[(31,25),(7,25),(7,34)],0,2)
route('D03','PVC sanitary','3','Main south','Suite3 group',[(31,51),(7,51)],0,2)
route('D04','PVC sanitary','3','Main south','Suite4 group',[(31,60),(21,67)],0,2)
route('D05','PVC sanitary','4','Main middle','Master group',[(31,32),(45,32),(54,45)],0,2)
for f in fixtures:
 if not f['waste_size']:continue
 x,y=f['x'],f['y']; typ=f['type'];
 if x<26: sx=7 if y<58 else 21;sy=34 if y<42 else 51 if y<58 else 67
 else:sx=31 if f['id'] in ['K1','K2'] else 54;sy=y if sx==31 else 32
 route('D-'+f['id'],'PVC sanitary',f['waste_size'],'Group branch',f['id'],[(sx,sy),(x,sy),(x,y)],2 if typ!='Toilet' else 1,1,'Provisional fixture waste branch; connect tocoordinatedgroup')
for i,(x,y) in enumerate([(8,34),(8,53),(20,65),(54,32)],1):
 route(f'V{i}','PVC vent','2.5','Fixture group','VTR-'+str(i),[(x,y),(x+2,y)],15.5,1,'Drawn2-1/2vent size retained; termination count provisional')
for id,q in [('Guest shower tie-ins',18),('Master doublelav added vent',24),('Island sink ventloop',20),('Porch sink vent branch',18),('Laundry/washer vent',20)]:
 route('V-'+id,'PVC vent','2',id,'Vent header',[(0,0),(q,0)],0,0,'Diagrammatic branch-length allowance; not mapcoordinates')
for size,p in [('1.5',1.8),('2',2.45),('2.5',7.5),('3',4.8),('4',7.713)]:
 rr=[r for r in routes if r['system'] in ['PVC sanitary','PVC vent'] and r['size']==size];q=sum(r['net_ft'] for r in rr)
 add('Plumbing','DWV piping','DWV'+size,f'Sch40PVC DWV pipe {size}inch',q,'LF',p,.05,'PP101/PP401; routes',calc='Sum '+', '.join(r['id'] for r in rr),waste=.10,pack=10,price='P01' if size=='4' else 'EST',risk=.3)
# fitting stock derived pergroup; not count-of-symbolclaim
for size,el90,el45,wye,tee,coup,clean in [('1.5',12,8,0,7,12,2),('2',15,12,6,8,20,4),('2.5',8,8,0,4,12,4),('3',8,10,4,0,12,2),('4',4,12,6,0,18,3)]:
 for typ,n,baseprice in [('longsweep90',el90,4),('45elbow',el45,3),('wye',wye,5),('sanitarytee',tee,4),('coupling',coup,2),('cleanoutadapter/plug',clean,7)]:
  if n:add('Plumbing','DWV fittings','D-'+size+'-'+typ,f'PVC DWV {size}inch {typ}',n,'EA',round(baseprice*(float(size)/1.5)**1.5,2),.16,'PP401 + estimating fittings register',basis='Derived provisional fittings',calc=f'{size}inch group connection/riser/turnstock; finalspoollayoutnotavailable',waste=.05)
for code,d,q,u,p,h in [('DRED43','4x3reducing wye',4,'EA',24,.22),('DRED42','4x2reducing wye',3,'EA',22,.22),('DRED32','3x2reducing wye',5,'EA',16,.22),('DRED21','2x1-1/2reducingbushing',8,'EA',3,.12),('DVRED','2-1/2x2ventreducer',8,'EA',8,.15),('COCOVER','Finishedcleanoutcover',8,'EA',20,.12),('DWCAP','TemporaryDWV roughcaps',24,'EA',3,.08),('DWGLUE','PVCcementquart',4,'EA',16,.1),('DWPRIM','PVCprimerquart',4,'EA',12,.1),('DWHANG','DWVclevishangers assorted',52,'EA',4,.1),('DWROD','3/8threadedrod',100,'LF',1.1,.02),('DWNUT','3/8nut/washer',208,'EA',.12,.008),('DWANCH','Hangerstructuralanchor',52,'EA',1.2,.06),('VBOOT','TPO-compatible2-1/2ventboot',4,'EA',45,.5),('VSTRAP','Ventwindrestraint',8,'EA',12,.12),('DWBED','Under-slab/servicepipebedding',8,'CY',45,.5),('DWEXC','Sanitarytrench/backfill',270,'LF',0,.18),('DWSLEEVE','4inchsanitaryexteriorwallseal',1,'EA',95,.6),('SANCON','Citysanitarytie-infittings',1,'kit',300,3)]:
 r=add('Plumbing','DWV installation',code,d,q,u,p,h,'PP101/PP401/PP501/PP502',calc='DWVinstallationallowance; no duplicatedfixturetraps')
 if code=='DWEXC':r['equipment_services']=800
# Mechanical observed device register from MH101. xy are normalized feet matching plan.
for id,room,typ,cfm,size,x,y in [
('SA01','Suite2','S-1',135,'8x6',11,27),('SA02','Study','S-1',120,'8x6',18,30),('SA03','Suite2 bath','S-2',35,'6round',7,34),('SA04','Suite3','S-2',85,'8x6',7,47),('SA05','Suite3 bath','S-2',40,'6round',9,56),('SA06','Suite4','S-1',140,'8x8',11,65),('SA07','Suite4 bath','S-1',45,'6x6',14,62),('SA08','Greatroom north','S-1',135,'8x6',30,41),('SA09','Greatroom middle','S-1',135,'8x6',30,49),('SA10','Greatroom south','S-1',135,'8x6',30,58),('SA11','Enclosedporch','S-1',135,'8x6',30,66),('SA12','Master','S-1',185,'8x8',51,65),('SA13','Masterbath','S-2',85,'8x6',55,47),('SA14','Mastercloset','S-2',40,'6round',56,52),('SA15','Laundry','S-2',80,'8x6',50,27),('D01','Unclassified device at eastwall','D-01',150,'unspecified',62,30),
('RA01','Suite2','R-1',170,'8x8',1,20),('RA02','Study','R-1',120,'8x6',18,20),('RA03','Westcloset/suite3','R-1',125,'8x8',11,43),('RA04','Greatroom north','R-1',205,'10x8',12,41),('RA05','Greatroom south','R-1',205,'10x8',12,58),('RA06','Suite4','R-1',185,'8x8',12,60),('RA07','Porch','R-1',135,'8x8',47,69),('RA08','Master','R-1',185,'8x8',61,58),('RA09','Laundry','R-1',80,'8x6',62,30)]:
 devices.append(dict(id=id,room=room,type=typ,cfm=cfm,neck=size,x=x,y=y,basis='Provisional added' if id=='SA17' else 'Observed MH101 tag',source='MH101; MH601',note='D-01 service/flowdirection requires confirmation' if id=='SA16' else 'RA06/RA07 feeds are riser/branchinterpretations; confirm tags' if id in ['RA06','RA07'] else ''))
 if id=='D01':continue
 # Counted device withconnectionkit; roundflexonlywhere used, plenumsseparate
 add('Mechanical',id+' '+room,'GRILLE-'+typ+'-'+size,f'Aluminum {typ} airterminal {size}, {cfm}CFM',1,'EA',55 if typ.startswith('S') else 65,.3,'MH101/MH601',basis=devices[-1]['basis'],calc=id+' one airterminal; finalneck/device selectionpending')
 add('Mechanical',id+' '+room,'BOOT-'+size,f'Sheetmetal terminalboot {size}',1,'EA',38,.3,'MH101/MH601',basis='Derived connection kit',calc=id+' one boot/connection plenum')
 if id.startswith('SA'):
  add('Mechanical',id+' '+room,'DAMP-'+size,f'Balancing damper / takeoff {size}',1,'EA',24,.2,'MH101/MH601',basis='Derived connection kit',calc=id+' one manualdamper')
# Straight duct centerlines from visible MH101 trunks; no flex substitution for long rigidruns.
def duct(id,sys,size,points,vertical=0,note='Scaled MH101 centerline'):
 w,h=[float(z) for z in size.split('x')];L=sum(hypot(b[0]-a[0],b[1]-a[1]) for a,b in zip(points,points[1:]))+vertical
 ducts.append(dict(id=id,system=sys,size=size,points=points,vertical_ft=vertical,length_ft=round(L,2),surface_sf=round(2*(w+h)/12*L,2),source='MH101/MH401',basis=note))
for args in [
('DS01','Supply','10x16',[(39,32),(30,32)],0),('DS02','Supply','12x10',[(30,32),(11,32)],0),('DS03','Supply','12x10',[(11,32),(11,44)],0),('DS04','Supply','10x8',[(11,44),(11,59)],0),('DS05','Supply','8x8',[(11,59),(11,66)],0),('DS06','Supply','12x10',[(30,32),(30,43)],0),('DS07','Supply','10x10',[(30,43),(30,58)],0),('DS08','Supply','8x6',[(30,58),(30,67)],0),('DS09','Supply','12x10',[(39,32),(51,32)],0),('DS10','Supply','10x10',[(51,32),(51,52)],0),('DS11','Supply','10x8',[(51,52),(51,63)],0),('DS12','Supply','8x6',[(51,32),(51,29),(49,29)],0),('DS13','Supply','12x10',[(39,32),(39,32)],10),
('DR01','Return','14x10',[(39,34),(23,34),(23,20),(18,20)],0),('DR02','Return','8x8',[(18,20),(1,20)],0),('DR03','Return','10x8',[(39,34),(14,34),(14,58)],0),('DR04','Return','8x8',[(14,58),(12,58)],0),('DR05','Return','10x10',[(39,34),(49,34),(49,69),(47,69)],0),('DR06','Return','8x8',[(49,58),(61,58)],0),('DR07','Return','8x6',[(49,34),(49,30),(62,30)],0),('DR08','Return','16x10',[(39,34),(39,34)],10)]:duct(*args)
# short terminalconnections, explicit net4ft, maxflex6ft schedule; 8inch equivalents are estimating adapters.
for f in devices:
 if f['id']=='D01' or f['id'].startswith('SA'):continue
 sz='6x6' if f['cfm']<=100 else '8x8' if f['cfm']<=200 else '10x10'
 duct('DT-'+f['id'],'Terminal',sz,[(f['x'],f['y']),(f['x']+4,f['y'])],0,'4ft rigid terminalconnection allowance; localneck transitionrequired')
for sz in sorted(set(d['size'] for d in ducts)):
 ds=[d for d in ducts if d['size']==sz];q=sum(d['length_ft'] for d in ds);w,h=map(float,sz.split('x'));sfper=2*(w+h)/12
 add('Mechanical','Ductwork','DUCT-'+sz,f'Galvanized rectangular duct {sz}inch, shopstraightsections',q,'LF',round(sfper*3.25,2),.12,'MH101/MH401; ductregister',basis='Measured / provisional',calc='Sum '+','.join(d['id'] for d in ds),waste=.05,pack=4,risk=.25)
 # actual fitting schedule by size generated independent of straightduct quantity
 n90=sum(max(0,len(d['points'])-2) for d in ds)+ (2 if sz in ['12x10','16x10'] else 0)
 if n90:add('Mechanical','Ductwork','DE90-'+sz,f'{sz} rectangular90elbow with turningvanes',n90,'EA',55+sfper*8,.65,'MH101/MH401',basis='Derived provisional fittings',calc='Planroutechanges plusAHU verticaloffsets')
for code,d,q,u,p,h,w,pk,calc in [
('TRANS','Rectangularduct size transition, assorted',14,'EA',48,.5,0,1,'Trunksteps andequipment/plenumconnections'),('DBRANCH','Rectangularbranch/shoetakeoff, assorted',12,'EA',35,.4,0,1,'Trunkbranchjunctions;terminaldampersseparate'),('ENDCAP','Duct endcap, assorted',10,'EA',15,.2,0,1,'Trunkends'),('PLENUM','AHU supply/return insulatedplenum',2,'EA',190,1,0,1,'2equipmentplenums'),('FLEXCON','AHU flexiblevibrationconnector',2,'EA',45,.3,0,1,'Supplyandreturn'),('DUCTINS','R8ductwrap withvaporjacket',sum(d['surface_sf'] for d in ducts)*1.25,'SF',.95,.018,.1,300,'Straightductsurface*1.25fittings;systemR-valuefinalcoordination'),('DTAPE','UL181foil ducttape',1200,'LF',.11,.005,0,150,'Seamsandinsulationjoints'),('MASTIC','Ductsealingmastic',10,'GAL',28,.2,0,1,'Alljointsealingprovision'),('SCLEAT','S-drivecleats andjointflanges',700,'LF',.8,.008,.1,10,'Shopsegmentjoints'),('DSCREW','Sheetmetalductscrews',2000,'EA',.045,.002,0,1000,'Jointfasteners'),('DHANG','Galvanizedducthangers',ceil(sum(d['length_ft'] for d in ducts)/4),'EA',4,.12,0,1,'4ftsupportspacingestimatingallowance'),('DHARD','Ducthangeranchor/rodkit',ceil(sum(d['length_ft'] for d in ducts)/4),'EA',6,.12,0,1,'Oneeachhanger'),('FILM','Ducttemporaryprotectionfilm',1,'roll',55,.5,0,1,'Constructiondustcontrol')]:
 add('Mechanical','Ductworkaccessories',code,d,q,u,p,h,'MH101/MH401/MH801/A110',calc=calc,waste=w,pack=pk)
# Equipment, refrigerant, drains, ventilation accessories
for code,d,q,u,p,h,w,pk,calc in [
('HP1','HP-1 4tonR454B heatpump, scheduledTrane',1,'EA',5200,3,0,1,'A5HL5048A1000A;allowance,notquote'),('AHU1','Matched4ton-class R454B airhandler inclTXV/A2Lfactorysafety',1,'EA',2600,4,0,1,'AHUscheduleempty;noapprovedmodel'),('HEATKIT','Matched5kW supplementalheaterkit',1,'EA',220,1,0,1,'A105legendallowance;coordinationhold'),('AHUSTAND','AHUstand/vibrationpads',1,'EA',180,.7,0,1,'Unitverticalclosetinstallation'),('HPPAD','Condenserconcretepad',1,'EA',190,.6,0,1,'MP101equipmentbase;notstructuraldesign'),('HPANCH','Engineeredcondenserwindanchor kit',1,'kit',120,.8,0,1,'Manufacturer/windapprovedanchorage'),('AHPAN','Secondarycondensatepan',1,'EA',65,.4,0,1,'Unitprotectionallowance'),('FLOAT','Primary/secondaryoverflowfloatswitch',2,'EA',30,.25,0,1,'2shutdownsafeties'),('THERMOSTAT','Heatpump thermostat withhumidity capability',1,'EA',180,.5,0,1,'Controlsequenceapprovalpending'),('TWIRE','18/8thermostat/controlcable',125,'LF',.45,.01,.1,250,'Stat50ft+outdoor75ft'),('FILTER','Replacementfilter set',3,'EA',28,.1,0,1,'Startuptwochanges+handover'),('FILTERBOX','Filterrack/cabinet',1,'EA',130,.5,0,1,'NotinAHUschedule'),('ACRLIQ','3/8OD ACRcoppertubing',50,'LF',2.2,.045,.1,50,'35ftplan+10ftrise+5ftconnection'),('ACRSUCT','7/8OD ACRsuctiontubing',50,'LF',5.8,.055,.1,50,'Diameterprovisionalmanufacturerselection'),('ACRINS','7/8suctiontube insulation',50,'LF',1.7,.025,.1,6,'Matchedlinesetlength;wallthicknessapprove'),('ACRUV','ExteriorlinesetUVjacket',18,'LF',2,.03,.1,6,'Outdoorverticalrun'),('ACR90L','3/8ACR longradiuselbows',6,'EA',4,.18,0,1,'Pathbends'),('ACR90S','7/8ACR longradiuselbows',6,'EA',10,.22,0,1,'Pathbends'),('ACRCOUPL','3/8ACRcouplings',3,'EA',3,.12,0,1,'50ftcoilsandconnections'),('ACRCOUPS','7/8ACRcouplings',3,'EA',6,.15,0,1,'Coils/connections'),('BRAZE','Brazingalloy',1,'LB',70,.2,0,1,'Connections;nitrogenpurgeincommissioning'),('DRIER','R454B-compatible liquidlinedrier',1,'EA',55,.25,0,1,'Verifyfactoryincluded;deductifprovided'),('ACRSUP','Linesetisolatingsupport',14,'EA',5,.1,0,1,'50ft/4ft plusends'),('PENAC','Linesetwallseal/sleeve',2,'EA',35,.35,0,1,'MP101riseaboveceiling'),('CDPIPE','3/4PVC condensate/pandrains',60,'LF',.7,.035,.1,10,'Primary35+secondary25ft;dischargeRFI'),('CD90','3/4PVC90elbow',12,'EA',.65,.1,0,1,'6pereachdrainroute'),('CDTEE','3/4PVCcleanouttee/cap',6,'EA',2,.12,0,1,'Directionchanges/unitaccess'),('CDTRAP','Manufacturer-matchedcondensatetrapkit',1,'EA',28,.35,0,1,'SizedtoAHUstaticpressure'),('CDCHECK','Condensatecheckvalveprovision',1,'EA',18,.15,0,1,'MP101note;confirmneedwithtraparrangement'),('CDINS','Condensateinsulation',35,'LF',.7,.02,.1,6,'Interiorprimaryroute'),('CDHANG','Condensatepipesupport',20,'EA',1.8,.08,0,1,'3ftspacingprovision'),('SOAK','Condensatesoakawaystone/receptor',1,'EA',65,.75,0,1,'Approvedterminationpending')]:
 add('Mechanical','Equipment / refrigerant / condensate',code,d,q,u,p,h,'MH101/MH601/MH801/MP101/A105',calc=calc,waste=w,pack=pk,price='P07' if code in ['HP1','AHU1'] else 'EST',risk=.3)
for i,loc,L in [(1,'Mastershower',7),(2,'Masterbath',7),(3,'Suite2bath',5),(4,'Suite3bath',7),(5,'Suite4bath',10)]:
 for code,d,q,u,p,h in [('EF','LorenCook GC-100exhaustfan allowance',1,'EA',350,.8),('EFDUCT','6x6galvanizedexhaustduct',L,'LF',6,.12),('EFEL','6x6exhaustelbow',2,'EA',30,.3),('EFHOOD','6x6walllouver/hood',1,'EA',65,.4),('EFDAMP','Exhaustbackdraftdamper',1,'EA',28,.2),('EFSUP','Exhaustductsupport',ceil(L/4)+1,'EA',5,.1)]:
  add('Mechanical',f'EF-{i:02} {loc}',code,d,q,u,p,h,'MH101/MH601',basis='Derived fan kit',calc=f'EF-{i:02}; fan50CFMat0.70inw.g.pendingverifiedcurve;wallroute{L}ft')
for code,d,q,u,p,h,calc in [('DRYDUCT','4inchrigiddryerexhaust',20,'LF',3.5,.12,'Laundrytoeastwall14ft+6ftvertical'),('DRYEL','4inchdryerlongsweepelbow',3,'EA',12,.25,'3routechanges'),('DRYHOOD','Dryerwalltermination withdamper',1,'EA',35,.4,'One;noscreen'),('DRYCON','Listeddryertransitionconnector',1,'EA',25,.25,'One;rigidtrunkabove'),('HOODDUCT','8inchgalvanizedcookingexhaust',50,'LF',8,.14,'Mainhood30ft+porch20ft'),('HOODEL','8inchexhaustelbow',6,'EA',32,.35,'3perhood'),('HOODCAP','8inchroof/walltermination',2,'EA',85,.6,'2cookingstations'),('HOODFLASH','Hoodpenetrationflashing',2,'EA',55,.6,'Compatiblewithenvelope'),('MUA','Makeupairmotorizeddamper/controller provision',1,'kit',450,2,'Actualhoodflowsandnecessitynotprovided'),('MUADUCT','8inchmakeupairduct',25,'LF',8,.14,'Allowance25ft'),('MUAFILT','Makeupairfilterbox/intake',1,'EA',140,.6,'Oneprovisionalintake'),('MUAEL','8inchmakeupairelbow',3,'EA',32,.35,'Routeallowance'),('HVACTAB','Airbalancingandductleaktest',1,'activity',0,16,'Allterminals;testequipmentfee$300'),('HVACSTART','Nitrogenpressuretest/evacuation/refrigerantcharge',1,'activity',150,12,'Consumables150+instrumentrental200;additionalchargebasedactualunit'),('HVACCOORD','MEPshoproutingcoordination',1,'activity',0,16,'Truss/chases/clearancescoordination')]:
 r=add('Mechanical','Ventilation / closeout',code,d,q,u,p,h,'A102/MH101/MH801',calc=calc,risk=.3)
 if code=='HVACTAB':r['equipment_services']=300
 if code=='HVACSTART':r['equipment_services']=200
# Framed acoustic screen separated from HVACequipment cost
for code,d,q,u,p,h in [('SCRPOST','PT4x4screenpost8ft',4,'EA',20,.4),('SCRSTUD','PT2x4screenframing',180,'LF',.75,.04),('SCRBOARD','5/16fibercementbothfaces',160,'SF',1.4,.04),('SCRFOAM','1inchsoundfoam',80,'SF',1.6,.015),('SCRCLIP','A33/SP1clips',40,'EA',3,.08),('SCRFOOT','Screenpostfootingconcrete',.5,'CY',210,2),('SCRFIX','Screenscrew/nailpack',1,'pack',45,.8)]:
 add('Mechanical','Acoustic screen',code,d,q,u,p,h,'A102/A110',calc='16LFx5ft estimatingframed screen;airflowclearancespending')
# Electrical room device schedule; visible symbol tally with explicit estimating completion.
# (room,x,y,standard duplex,wet/GFI-protected duplex,downlight,wetdownlight,pendant,vanity,fan roughbox,1pole,3way)
erooms=[]
for v in [
('Suite2',6,24,5,0,4,0,0,0,1,2,0),('Study',21,24,5,0,4,0,0,0,1,2,0),('Suite3',6,44,5,0,4,0,0,0,1,2,0),('Suite4',6,63,5,0,4,0,0,0,1,2,0),('Master',57,63,5,0,4,0,0,0,1,2,0),('Greatroom',32,48,8,0,4,0,0,0,1,2,4),('Enclosedporch',35,65,3,4,3,0,0,0,2,3,2),('Kitchen',40,37,0,10,5,0,3,0,0,3,2),('Pantry',45,28,0,4,2,0,0,0,0,1,0),('Laundry',57,27,0,5,2,0,0,0,0,1,0),('Masterbath',57,40,0,4,0,2,1,3,0,5,0),('Suite2bath',5,34,0,2,0,1,0,1,0,3,0),('Suite3bath',5,53,0,2,0,1,0,1,0,3,0),('Suite4bath',18,65,0,2,0,1,0,1,0,3,0),('Masterclosets',55,52,1,0,4,0,0,0,0,2,0),('Westclosets/linen',11,44,0,0,4,0,0,0,0,4,0),('Entry/halls/dropoff',28,29,5,0,7,1,0,0,0,3,4),('Garage',50,12,0,7,0,0,0,0,0,2,2),('Exterior',62,40,0,9,0,0,0,0,0,3,0)]:
 keys=['room','x','y','duplex','protected_duplex','downlight','wet_downlight','pendant','vanity','fan_box','single_pole','three_way']
 r=dict(zip(keys,v));r['basis']='Manual room symbol interpretation / completion allowance; not verified device schedule';r['source']='A105';erooms.append(r)
 for key,code,desc,price_,hours in [('duplex','REC','20A tamper-resistant duplex',4.5,.12),('protected_duplex','RECP','20A TR duplex on protected circuit; exterior WR below',5.5,.12),('downlight','LED','Recessed LED downlight with driver/junctionbox',22,.22),('wet_downlight','LEDW','Wet-location recessed LED withdriver/junctionbox',30,.25),('pendant','PEND','Pendant/decorativefixture allowance',180,.45),('vanity','VANLIGHT','Bathvanityfixture',95,.35),('fan_box','FANBOX','Metal fan-rated boxandbrace; fanexcludedpernote',28,.35),('single_pole','SW','Singlepole decora switch',4.5,.1),('three_way','SW3','Threeway decora switch',8,.12)]:
  if r[key]:add('Electrical',r['room'],code,desc,r[key],'EA',price_,hours,'A105; electrical roomregister',basis='Provisional room tally',calc=f"{r['room']}: {r[key]} {key}; devicefamilyfinalselection")
 # boxes &plates receptacles only; switches grouped elsewhere
 n=r['duplex']+r['protected_duplex']
 if n:
  add('Electrical',r['room'],'RECBOX','Deep devicebox withsteel-studmount',n,'EA',4.2,.18,'A105',basis='Derived device kit',calc=f'{n}receptacles,1boxeach;exteriormodifierbelow')
  add('Electrical',r['room'],'RECPLATE','Receptaclefaceplate',n,'EA',1.2,.04,'A105',basis='Derived device kit',calc='Oneperreceptacle;wetlocationscoverupgradebelow')
# no fan motor purchases; notes explicitly electricalroughinonly
for code,d,q,u,p,h,calc in [
('SWBOX1','1gangswitchbox',20,'EA',4,.18,'20single-gangstations'),('SWBOX2','2gangswitchbox',14,'EA',6,.22,'14two-gangstations'),('SWBOX3','3gangswitchbox',6,'EA',8,.26,'6three-gangstations;capacity66gangs'),('SWPL1','1gangswitchplate',20,'EA',1.2,.04,'Matchingstations'),('SWPL2','2gangswitchplate',14,'EA',2,.04,'Matchingstations'),('SWPL3','3gangswitchplate',6,'EA',3,.05,'Matchingstations'),('DIMUP','Dimmersupgrade premium overbasic switch',12,'EA',25,.05,'12switches replacebasics;incrementalpriceonly'),('FANTIMER','Exhausttimer switchupgrade',5,'EA',22,.05,'5fancontrols;replacesbase switch'),('WRUP','Weatherresistantreceptaclepremium',9,'EA',3,0,'Exterior9protectedreceptacles'),('WPUP','Weatherproofbox/extra-dutycoverupgrade',9,'EA',22,.18,'Replacesordinarybox/plate;priceincrementonly'),('GSTRIP','GarageLED linearfixture',3,'EA',65,.35,'3drawnrectangularfixtures'),('FLOOD','ExteriorLEDflood',4,'EA',85,.45,'4cornerfloods'),('SCONCE','Exteriorwalllight',4,'EA',110,.45,'4WPwallfixtures'),('FIXBOX','Octagonalfixturebox withsupport',22,'EA',5,.18,'Pendant/vanity/exterior/linearboxes;recesseddriverboxesincluded'),('SMOKE','Hardwiredphotoelectricsmoke/COalarm',7,'EA',55,.25,'4bedrooms+2circulation+study allowance;notcountedcodecompliance'),('SMBOX','Smokealarmceilingbox',7,'EA',3.5,.15,'Oneperalarm'),('FLOORBOX','Completefloorduplextamperresistantbox',2,'EA',75,.55,'Greatroomflooroutletprovision'),('RANGEOUT','50A4wirecookingreceptacle/box',2,'EA',35,.4,'Mainandporchelectriccooking'),('DRYOUT','30A4wiredryerreceptacle/box',1,'EA',32,.4,'Laundrydryer'),('JBOX','Accessible4inchsquarejunctionbox/cover',12,'EA',7,.25,'Splices/equipmentconnections'),('NMCON','NMcableclampconnector',350,'EA',.4,.025,'Panel/boxterminations'),('GROMMET','Steelstudgrommet',650,'EA',.3,.012,'Steelstudpenetrations'),('NAILPL','Cableprotectionplate',160,'EA',.65,.04,'Vulnerableframingcrossings'),('STAPLE','Listedcablesupport/staple',1500,'EA',.06,.006,'Cablelength/supportspacing allowance'),('PIGTAIL','Coppergroundpigtail/screw',190,'EA',.55,.035,'Metalboxes/fixturegrounds'),('WIRENUT','Listedspliceconnector',1200,'EA',.12,.01,'Device/panel/junctionsplices'),('SEALE','Electricalpenetrationsealant',10,'tube',18,.12,'Envelope/fireseparationpenetrations'),('LABEL','Circuit/deviceidentificationlabels',200,'EA',.15,.025,'Traceability/labeling'),('DOORBELL','Video-doorbellpowertransformer/chimeprovision',1,'EA',75,.6,'Ringcamera powerrequested;cameraseparateallowance'),('BELLWIRE','18/2doorbellwire',70,'LF',.18,.012,'Paneltoentryrouting'),('RING','Entrypoweredsmartcameraallowance',1,'EA',150,.3,'A105ownerdeviceallowance'),('LANDTRANS','Landscape-lighttransformer/timer',1,'EA',180,.5,'A105landscapepower'),('LANDCABLE','12/2landscapeLVcable',200,'LF',.55,.015,'Provisional4zones*50ft'),('LANDLIGHT','Landscapelightfixture',8,'EA',55,.3,'Ownerselectionallowance')]:
 add('Electrical','Devices / accessories',code,d,q,u,p,h,'A105; estimatingdevices',calc=calc)
# proposed circuit runs: home run Manhattan from panel(48,22), up/down22ft, plus explicitly estimated branch distribution
# assign all room-general device circuits and appliance feeds; panelspacesvalidationonly,noloadapproval.
def circuit(name,amps,poles,wire,x,y,branch=0,protect='DF',panel='M',note='Estimating circuit; no load calculation'):
 id=f'C{len(circuits)+1:02}';px,py=(48,22) if panel=='M' else(30,50)
 horizontal=abs(px-x)+abs(py-y);vertical=22;slack=6;total=horizontal+vertical+slack+branch
 circuits.append(dict(id=id,panel=panel,load=name,amps=amps,poles=poles,protection=protect,cable=wire,x=x,y=y,horizontal_ft=horizontal,vertical_ft=vertical,branch_ft=branch,termination_ft=slack,net_ft=total,basis=note))
for r in erooms:
 if r['duplex'] and r['room']!='Enclosedporch':circuit(r['room']+' generalreceptacles',20,1,'12/2',r['x'],r['y'],(r['duplex']-1)*10,panel='S' if r['y']>45 else'M')
for name,x,y,br in [('Kitchen smallapplianceA',40,37,45),('Kitchen smallapplianceB',40,37,45),('Porch smallappliance',47,64,35),('Pantry counter',45,28,30),('Laundry receptacles',57,27,35),('Masterbath receptacles',57,40,24),('Guestbaths2+3',5,34,30),('Suite4bath receptacles',18,65,15),('Garage receptacles/operators',50,12,65),('Exterior/ChristmaslightsA',0,35,85),('Exterior/ChristmaslightsB',63,35,85)]:circuit(name,20,1,'12/2',x,y,br)
for name,x,y,br in [('Kitchen/porchlights',40,40,100),('Westbedroomslights',8,43,150),('Master/closetslights',55,50,100),('Greatroom/entrylights',32,42,120),('Bathlights/fans',25,45,140),('Garage/exteriorlights',50,15,140)]:circuit(name,15,1,'14/2',x,y,br,panel='S')
circuit('Interconnectedsmokealarms',15,1,'14/3',28,44,130,panel='S')
for name,x,y in [('KitchenREF',34,35),('PantryREF',48,27),('Dishwasher1',37,41),('Dishwasher2',49,27),('Disposal1',40,41),('Disposal2',47,63),('Microwave',43,34),('Porchmicrowave',47,65),('Winecooler',47,63),('Mainhood',39,34),('Porchhood',47,67),('Landscape/irrigation',62,25)]:circuit(name,20,1,'12/2',x,y)
for name,a,w,x,y,prot in [('Main electricrange',50,'6/3',40,35,'GFCI'),('Porch electriccooktop',40,'8/3',47,67,'GFCI'),('Dryer',30,'10/3',61,29,'GFCI'),('WH-1 provisional',40,'8/2',38,23,'GFCI'),('HP-1 provisional',50,'8/2',65,46,'GFCI'),('AHU1+5kW provisional',40,'8/2',40,30,'GFCI')]:circuit(name,a,2,w,x,y,0,prot)
circuit('D-01 unclassified equipment provision',20,1,'12/2',62,30)
# add switchlegs / travelers as independent measured allowance, not duplicated in lightingbranch circuits
wiretot=defaultdict(float)
for c in circuits:wiretot[c['cable']]+=c['net_ft']
wiretot['14/3']+=300 # 8fanroughins*20ft+140ftthreewaytravelers;lightingbranches exclude thesetravelers
for wire,p,pack in [('12/2',.724,250),('14/2',.496,250),('14/3',.80,250),('10/3',2.1,100),('8/3',3.3,50),('8/2',2.65,50),('6/3',4.8,50)]:
 q=wiretot[wire]
 if q:add('Electrical','Branchcable','NM-'+wire,f'Copper NM-B {wire}withground, dryinterior',q,'LF',p,.014 if wire in ['12/2','14/2','14/3'] else .026,'Proposedcircuitregister',calc='Sumcircuitnetfeet'+(' +300ftfanlegs/threewaytravelers' if wire=='14/3' else''),waste=.12,pack=pack,price='P08' if wire in ['12/2','14/2'] else'EST',risk=.3)
# breakers by actual proposedcircuit counts, protection atbreakeravoidsdoubleGFCIdevicepurchase
breaks=defaultdict(int)
for c in circuits:breaks[(c['amps'],c['poles'],c['protection'])]+=1
for (a,p,prot),n in breaks.items():
 add('Electrical','Panelbreakers',f'BR-{a}-{p}-{prot}',f'{a}A {p}-pole {prot}breaker, panelfamilymatched',n,'EA',96 if p==1 else 165,.15,'Proposedcircuitregister',basis='Derived provisional',calc=f'{n}proposedcircuits;ratingandprotectionengineeringhold',price='P05' if a==20 and p==1 else'EST')
for code,d,q,u,p,h,calc in [
('METER','200Ameter/socket/service disconnectenclosure',1,'EA',650,3,'Service200Aallowance;utilityapprovedequipmentrequired'),('PANEL','200A60spaceinteriorloadcenter/trim',1,'EA',450,3,'Mpanel60spaces;loadcalculationpending'),('SUBPANEL','100A24space subpanel/trim',1,'EA',180,2,'Sparecapacityandshorterbranches;100Afeederloadcheckpending'),('FEEDBR','100A2pole subpanelfeederbreaker',1,'EA',80,.2,'Mainpanel2spaces'),('SPD','Type2surgeprotective deviceand2polebreaker',1,'EA',170,.6,'Mainpanel2spacesreserved'),('SERVCU','2/0copperTHWN-2 serviceconductors',90,'LF',5.5,.025,'3conductors*30ftroute;service200Aprovisional'),('SERVPVC','2inchPVCserviceconduit',30,'LF',2.8,.1,'Meter/service30ftroute'),('SERVEL','2inchPVClongradiuselbow',4,'EA',18,.25,'Serviceoffsets'),('SERVCOUP','2inchPVCcoupling',6,'EA',2.2,.1,'Routejoints'),('SERVAD','2inchterminaladapter/bushing',4,'EA',4,.12,'Equipmentends'),('FEEDCU','#3copperTHWN2 subpanelfeeder',150,'LF',2.3,.02,'3conductors*50ftroute'),('FEEDGND','8AWGcopperfeederground',50,'LF',.85,.012,'One*50ftroute'),('FEEDPVC','1-1/2conduitforfeeder',50,'LF',1.9,.09,'50ftroute'),('FEEDEL','1-1/2conduitelbow',4,'EA',12,.22,'Fourbends'),('FEEDFIT','1-1/2conduitadapter/coupling',12,'EA',2.5,.1,'Jointsandends'),('GROUND','4AWGbarecopper grounding/bonding',100,'LF',1.25,.02,'Ufer/rods/waterbondingprovision'),('ROD','5/8x8ftgroundrod',2,'EA',28,.5,'Groundingelectrodesallowance'),('RODCLAMP','Listedgroundrodclamp',2,'EA',8,.08,'Oneperrod'),('BONDCLAMP','Ground/bondclamp',4,'EA',12,.1,'Foundation/metalservices'),('GBAR','Subpanelisolatedgroundbar',1,'EA',18,.3,'One'),('BONDJ','Enclosurebondingjumper/lugs',4,'EA',12,.12,'Serviceequipment'),('DISC','60AnonfusibleHPdisconnect',1,'EA',32,.5,'HP1'),('DISCWH','2poleWHlocaldisconnect',1,'EA',35,.4,'WH1'),('DISCAHU','AHUlocaldisconnect',1,'EA',35,.4,'AHU1'),('LFNC','3/4liquidtightconduit',30,'LF',1.2,.05,'3equipmentwhips*10ft'),('LFNCCON','3/4liquidtightconnector',6,'EA',4,.12,'2perwhip'),('WHIPCU','8AWGcopperTHWN-2whipconductors',60,'LF',.85,.012,'2hotconductors*3*10ft'),('WHIPGND','10AWGcopperwhipground',30,'LF',.45,.012,'3*10ftgrounds'),('EXTCOND','3/4exteriorPVCconduit',140,'LF',.8,.07,'Exteriorpower/Christmasroutes;NMterminatedinside'),('EXTWIRE','12AWGTHWN-2exteriorconductors',420,'LF',.25,.008,'3conductors*140ftexteriorroute'),('EXTFIT','3/4PVCcoupling/adapter',30,'EA',.8,.08,'Conduitends/joints'),('EXTEL','3/4PVC90elbow',12,'EA',2.5,.15,'Exteriorroutebends'),('STRAPE','Conduitstraps',80,'EA',.5,.06,'Service/feeder/exterior'),('DUCTSEAL','Electricalductseal',4,'LB',5,.1,'Entrysealing'),('ETEST','Continuity/polarity/protection/torque tests',1,'activity',0,20,'Electricalcommissioning;testequipment200'),('ECOORD','Circuitlayout/ownerwalkthrough/as-built',1,'activity',0,20,'Walkthroughplusdevice/circuitlabelrecords')]:
 r=add('Electrical','Service / feeders / equipment',code,d,q,u,p,h,'A105; estimatingcircuitlayout',calc=calc,risk=.3)
 if code=='ETEST':r['equipment_services']=200
# lowvoltage, modestwholehouse provisions
for code,d,q,u,p,h,pack in [('CAT6','Cat6riser-ratedUTPcable',1200,'LF',.23,.009,1000),('DATJACK','Cat6keystonejack',16,'EA',4,.12,1),('DATBOX','Lowvoltagebracket/plate',8,'EA',5,.18,1),('PATCH','16portpatchpanel',1,'EA',55,.5,1),('DATCAB','Structuredmediacabinet',1,'EA',110,.8,1),('DATCON','1inchcommunicationsconduit',80,'LF',.65,.06,10),('DATFIT','1inchconduitfittings',16,'EA',1.5,.1,1),('DATTST','Datacabletestandlabel',8,'run',0,.25,1)]:
 add('Electrical','Lowvoltage',code,d,q,u,p,h,'OwnerlocationsperA105',calc='8dataruns*150ft;networkelectronicsnotincluded',pack=pack)
# appliances: material standalone factoryassemblies, no duplicatedMEPtradeconnections
for code,d,q,p,h in [('REF','Refrigerator',2,1700,.5),('DW','Dishwasher',2,700,.4),('RANGE','Main electricrange',1,1600,.5),('COOKTOP','Porchelectriccooktop',1,800,.5),('MICRO','Microwave',2,300,.3),('WINE','Winecooler',1,600,.3),('HOODAPP','Cookinghood appliance',2,500,.5),('WASHER','Clotheswasher',1,850,.4),('DRYER','Electricdryer',1,800,.4)]:
 add('Appliances','Ownerselection',code,d,q,'EA',p,h,'A102/A105',basis='Counted / provisionalselection',calc='Delivery/unpack/positiononly;MEPconnectionselsewhere',risk=.35)
# Generalconditions/internalteam support. No subcontractor markup added tointernalhourly rates.
for code,d,q,u,p,h,eq,calc in [('SUPERVISE','Internalworking superintendent',480,'HR',0,1,0,'24weeks*20hr;tradeproductivehoursseparate'),('COORD','Procurement/scheduling/projectadmin',160,'HR',0,1,0,'24weeks~6.7hr'),('TEMPPOWER','Temporarypowerpole/service/rental',1,'service',0,8,1400,'Temporaryserviceallowance;utilitypermanentfeesexcluded'),('TEMPWATER','Temporarywater/hoses',1,'kit',250,4,0,'One'),('TOILET','Portabletoiletrental',6,'month',0,0,1080,'6months'),('DUMP','Wastecontainers/hauls',6,'load',0,0,3300,'6pulls'),('LIFT','Scissorliftrental',4,'week',0,0,2000,'Highceilings;scaffoldseparate'),('FREIGHT','Bulkfreight/deliveryreserve',1,'service',0,0,3500,'Materialsrateexcludesdeliveryexceptsupplierstatedincluded'),('CLEAN','Finalconstructionclean',SLAB,'SF',.08,.012,0,'3797SF'),('PROTECT','Floorsurfaceprotection',AC,'SF',.2,.008,0,'Finisharea'),('SMALLTOOLS','Smalltoolwear/PPE/sawblades',1,'kit',1800,0,0,'Wholejobconsumablesnotitemizedelsewhere'),('PERMIT','Permit/impact/utilityfees provisional reserve',1,'allowance',0,0,12000,'AllowanceONLY;noapprovedfeeschedule/sitebasis'),('BUILDTEST','Blowerdoor/finaltestingservice',1,'service',0,0,500,'One'),('SHOP','AdditionalMEP/shopengineeringreserve',1,'service',0,0,4000,'Electrical/MEPcoordinationandclarification;originaldesignfeesexcluded')]:
 add('General','Internaldelivery',code,d,q,u,p,h,'Estimatingbasis',calc=calc,equip=eq,risk=.35)
# More precise compatible tee accounting replaces generic count.
rows[:]=[r for r in rows if not r['item_code'].startswith('CTEE-')]
tees=defaultdict(int)
for r in routes:
 if not r['id'].startswith(('C-','H-')):continue
 f=next(f for f in fixtures if f['id']==r['to_node'])
 hs='1' if r['system']=='CPVC hot' and f['x']<26 else'0.75' if r['system']=='CPVC hot' and f['y']>45 else'1.5'
 tees[(hs,r['size'])]+=1
for (hs,bs),n in tees.items():add('Plumbing','Waterdistribution',f'CTEE-{hs}-{bs}',f'CPVC CTS {hs}x{hs}x{bs} reducingtee',n,'EA',8 if hs=='1.5' else 5,.18,'Fixturefeedroutes',basis='Derived provisionalfittings',calc=f'{n}branchfeedstohot/coldheaders;matchingCTSfamily')
price('P08','SupplyHouse SouthwireNM-Bcable','https://www.supplyhouse.com/Romex-Cable-NM-B-26361000','12/2 $180.99/250ft;14/2 $124/250ft','0.724/0.496 perft','Publicretailbenchmarks;shipping/tax/localavailabilityunconfirmed.')
price('P09','SupplyHouse18/8thermostatcable','https://www.supplyhouse.com/Electrical-Wire-20670000/','$152.73/250ft',.61092,'Adopted250ftcoil;noverifiedlocalquote.')
for r in rows:
 if r['item_code']=='TWIRE':r['material_rate']=.61092;r['price_source']='P09'
# Flexible supply tails are drawn on MH401; maximum6ft per MH601, estimate4ft each.
for dia in [6,8]:
 n=sum(1 for d in devices if d['id'].startswith('SA') and (6 if d['cfm']<=100 else 8)==dia)
 add('Mechanical','Supplyterminaltails','FLEX'+str(dia),f'R8flexduct {dia}inch round',n*4,'LF',2.5 if dia==6 else 3,.06,'MH401/MH601',calc=f'{n}supplies*4ft, below6ftmax;noextraterminalrigidduct',waste=.05,pack=25)
 add('Mechanical','Supplyterminaltails','COLLAR'+str(dia),f'Round{dia}inch-to-rectangularneckadapter',n,'EA',18,.2,'MH401/MH601',calc='Onetofinalboot;terminaldamperpricedseparately')
 add('Mechanical','Supplyterminaltails','FLEXCLAMP'+str(dia),f'Flexductclamp{dia}inch',n*2,'EA',2,.06,'MH401/MH601',calc='Twoendclampspertail')
for code,desc,q,u,p,h in [('D01EQ','D-01 unclassified150CFMdevice allowance',1,'EA',850,2),('D01DUCT','D-01 connectionductprovision8inch',25,'LF',8,.12),('D01CTL','D-01 control/intake/filterconnectionprovision',1,'kit',250,1),('D01DRAIN','D-01 drainconnectionprovision',20,'LF',1.5,.06)]:
 add('Mechanical','D-01 designhold',code,desc,q,u,p,h,'MG101/MH101/MH401',calc='Equipmenttype/modelundefined;allowancekeepsscopevisible;confirmwhetherdehumidifier/ventilationdevice',risk=.5)
# Elevation/details completion after cross-sheet review.
for code,desc,q,u,p,h in [('STONE','Decorative front-elevation stone veneer',250,'SF',9,.10),('STONEM','Stone veneer setting mortar80lb',18,'bag',20,.20),('STONEL','Stone veneer lath/anchors',250,'SF',1.2,.02)]:
 add('Finishes','Frontaccentpanels',code,desc,q,u,p,h,'A103front elevation',calc='250SFscaledfrontportal/featurebandallowance;subtracts250SFstuccofinishbelow',waste=.1 if code=='STONE' else 0,risk=.35)
for r in rows:
 if r['item_code']=='STUCCO':r['net_qty']-=ceil(250/14)
 if r['item_code']=='PAINTEX':r['net_qty']-=ceil(250*3/300)
 if r['item_code']=='DOOR-D26':r['material_rate']=4500;r['calculation']+='; modern glazed/impact appearance premium fromA103'
 if r['item_code']=='DOOR-D27':r['material_rate']=7500;r['calculation']+='; modern glazed/impact appearance premium fromA103'
for code,desc,q,u,p,h in [('WHTEE','3/4lead-free water heater connection tee',3,'EA',8,.15),('WHUNION','3/4water heater union/adapter',4,'EA',12,.15),('WHTEMP','Water heater temperature gauge / well',1,'EA',28,.2),('WHCHECK','3/4checkvalve water heater provision',1,'EA',25,.2),('WHBYP','Water shutoff sensor bypass / adapter provision',1,'kit',120,.6),('HBBOX','Hose bibb stainless recessedbox upgrade',5,'EA',90,.4),('PIPESHIELD','Pipe insulation supportshield',80,'EA',2.5,.06),('DCACCESS','Duct accessdoor',4,'EA',35,.3),('RABDAMP','Returnbranch balancingdamper',8,'EA',28,.25),('CSADDLE','Pipehanger threadedrod/channel allowance',1,'kit',180,3),('WATERBR','Water pipe expansionoffset/guide provision',1,'kit',120,2)]:
 add('Plumbing' if not code.startswith(('DC','RAB')) else'Mechanical','Detail completion',code,desc,q,u,p,h,'PP501/MH501/PG001',calc='Explicitdetailaccessoryprovision;finalhardware/submittalcoordination')
issue('Generic details include inapplicable commercial systems','MH501/MH502/MH701 andPG001 include surgical-suite, chilledwater, seismic-joint, fire-alarm andmedical-gas template details without associated residence systems.','Do notcount everygenericdetail asinstalledscope. Carryresidentialequipment/supports/controls only;no BAS,FACP,medicalgasorhydronicplant.','Engineeridentifiesapplicabledetails andcontrols;confirmsductsmokedetection/airflowthresholds andresidentialrequirements.')
# finish IDs stable after removing supersededtee lines, calculate cash purchasing andlabor.
for i,r in enumerate(rows,1):
 r['id']=f'B{i:04}'
 r['purchase_qty']=ceil(round(r['net_qty']*(1+r['waste'])/r['pack'],9))*r['pack']
 r['packs']=r['purchase_qty']/r['pack']
 r['material_cost']=round(r['purchase_qty']*r['material_rate'],2)
 r['labor_hours']=round(r['net_qty']*r['labor_hours_per_unit'],3)
 r['labor_cost']=round(r['labor_hours']*r['labor_rate'],2)
 r['base_cost']=round(r['material_cost']+r['labor_cost']+r['equipment_services'],2)
 r['low_cost']=round(r['base_cost']*(1-r['risk']),2);r['high_cost']=round(r['base_cost']*(1+r['risk']),2)
 r['release']='HOLD - engineering/selection/review' if r['trade'] in ['Plumbing','Mechanical','Electrical','Framing','Roofing','Openings'] else 'VERIFY quantity / selection before order'
exclusions=[
 dict(scope='Land, financing and realestate costs',treatment='Excluded',reason='Buildcostscopeonly'),dict(scope='Pool structure/equipment/heating/bonding/pooldeck/screen',treatment='Excluded',reason='A102poolBYOTHERS;separatevendorpackage'),dict(scope='Existingbuildingdemolition/hazardousmaterials',treatment='Excluded',reason='Noexistingbuildingdemolitionscopeprovided'),dict(scope='Deepfoundations,groundwaterdewatering,soilremediationandbulkgradefillabove6in',treatment='Excluded',reason='Noapplicablecurrentgeotech/civilmodel'),dict(scope='Offsiteutilityextension/roadcut/municipalcapacityupgrades',treatment='Excluded',reason='70ftonsiteserviceallowancesonly'),dict(scope='Gas/LPsystem,generator,PV,battery,EVcharging',treatment='Excluded',reason='Nosystemsdesignprovided;electriccookingassumed'),dict(scope='Ceilingfanmotors/blades',treatment='Excluded',reason='A105explicitrough-inonly;8fanboxesbudgeted'),dict(scope='Networkingactiveelectronics,wholehomesecurity/audio,windowtreatmentsandfurniture',treatment='Excluded',reason='PassiveCat6/doorbellallowanceonly'),dict(scope='Builderprofit/corporateoverheadmarkup',treatment='Excluded',reason='Internaldeliveryrequested;explicitonsitesupervision/adminincluded'),dict(scope='Permit/impact/utilityfees',treatment='12000 provisionalreserveincluded',reason='Notalocalfeescalculation;replacewithactuals'),dict(scope='Initialarchitecturaldesign/landscape/poolspecialtyengineering',treatment='Excluded',reason='4000additionalMEP/shopcoordinationreserveandtrussengineeringincluded')]

def export():
 DATA.mkdir(parents=True,exist_ok=True)
 sets={'bom':rows,'routes':routes,'fixtures':fixtures,'ducts':ducts,'circuits':circuits,'electrical_rooms':erooms,'air_devices':devices,'measurements':measures,'openings':openings,'rfis':rfis,'price_sources':prices,'exclusions':exclusions}
 for name,records in sets.items():
  (DATA/(name+'.json')).write_text(json.dumps(records,indent=2))
  if records:
   with (DATA/(name+'.csv')).open('w',newline='') as f:
    w=csv.DictWriter(f,fieldnames=records[0].keys());w.writeheader();w.writerows(records)
 source=[]
 for p in sorted(Path('work/samples/temple').glob('*.pdf')):source.append(dict(file=str(p),bytes=p.stat().st_size,sha256=hashlib.sha256(p.read_bytes()).hexdigest()))
 (DATA/'sources.json').write_text(json.dumps(source,indent=2))
 return sets
if __name__=='__main__':
 export();print(len(rows),'BOMrows',len(routes),'piperoutes',len(circuits),'circuits',round(sum(r['base_cost'] for r in rows)))
