from pathlib import Path
import math,csv,json
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from svg_accessibility import describe_svg, overlay_visible_title
OUT=Path(__file__).resolve().parent; G=9.80665

def hover(mass,area,rho):
    if min(mass,area,rho)<=0:raise ValueError('Mass, area and density must be positive')
    t=mass*G;v=math.sqrt(t/(2*rho*area));mdot=rho*area*v
    return dict(mass_kg=mass,disk_area_m2=area,density_kg_m3=rho,thrust_N=t,disk_loading_Pa=t/area,induced_disk_speed_m_s=v,far_wake_speed_m_s=2*v,mass_flow_kg_s=mdot,ideal_induced_power_W=t*v)
rows=[hover(float(m),float(a),rho) for m in [1,5,20] for a in np.geomspace(.1,10,81) for rho in [.9,1.225,1.4]]
with (OUT/'hover-sweep.csv').open('w',newline='',encoding='utf-8') as f:
    w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
# Independently reconstruct thrust from outgoing momentum and power from wake kinetic energy.
err_t=max(abs(r['mass_flow_kg_s']*r['far_wake_speed_m_s']/r['thrust_N']-1) for r in rows)
err_p=max(abs(.5*r['mass_flow_kg_s']*r['far_wake_speed_m_s']**2/r['ideal_induced_power_W']-1) for r in rows)
assert max(err_t,err_p)<1e-14
p=lambda m,a,r: hover(m,a,r)['ideal_induced_power_W']
assert abs(p(2,1,1.225)/p(1,1,1.225)-2**1.5)<1e-14
assert abs(p(1,4,1.225)/p(1,1,1.225)-.5)<1e-14
assert abs(p(1,1,4*.9)/p(1,1,.9)-.5)<1e-14
for args in [(0,1,1),(1,0,1),(1,1,0),(-1,1,1)]:
    try:hover(*args)
    except ValueError:pass
    else:raise AssertionError('Invalid parameter accepted')
# Symmetric noninteracting equal-loaded disks give the same total ideal power as their summed area.
assert abs(4*p(5/4,1/4,1.225)-p(5,1,1.225))<1e-12
meta={'title':'The ideal power cost of hovering','kind':'Deterministic actuator-disk teaching calculation, not measurements','g_m_s2':G,'parameters':{'mass_kg':[1,5,20],'area_m2_range':[.1,10],'area_points':81,'density_kg_m3':[.9,1.225,1.4]},'rows':len(rows),'formulas':{'thrust':'T=m*g','disk_speed':'vi=sqrt(T/(2*rho*A))','far_wake_speed':'vw=2*vi','mass_flow':'mdot=rho*A*vi','ideal_power':'Pi=T*vi=T^(3/2)/sqrt(2*rho*A)'},'baseline':hover(5,1,1.225),'assumptions':['Steady axial hover in still air, far from ground, ceilings, walls and other rotors.','Open unshrouded ideal disk, uniform loading and inflow, incompressible inviscid flow, no wake swirl.','All supported mass is included and vertical thrust equals weight. Disk area is swept area, not total blade surface area.','For multiple disks, summing area requires noninteracting equal disk loading; overlapping or coaxial disks cannot simply be added.'],'limitations':['Ideal induced power is a lower-bound reference for this open-disk hover model, not a universal lower bound for every lift mechanism.','Actual rotor shaft power also pays for blade profile drag, nonuniform inflow, swirl and other aerodynamic losses; electrical input adds motor and controller losses.','No empirical efficiency is assumed or fitted. No prediction of battery endurance, motor rating or measured flight performance.','No ground effect, forward flight, wind, transient manoeuvre, acoustics, blade stress or vehicle stability is modelled.','Density values are independent illustrative inputs, not an altitude or weather model.'],'validation':{'momentum_relative_error_max':err_t,'wake_energy_relative_error_max':err_p,'mass_power_exponent_1_5':True,'quadruple_area_halves_power':True,'quadruple_density_halves_power':True,'equal_loaded_noninteracting_disk_sum':True,'nonpositive_inputs_rejected':True},'sources':[{'title':'NASA Glenn Propeller Analysis, momentum and disk velocity','url':'https://www.grc.nasa.gov/WWW/K-12/BGP/propanl.html'},{'title':'NASA Glenn Propeller Thrust, pressure jump and ideal disk assumptions','url':'https://www1.grc.nasa.gov/beginners-guide-to-aeronautics/propeller-thrust/'},{'title':'NASA NDARC Theory Appendix1, induced velocity and power models','url':'https://ntrs.nasa.gov/api/citations/20160012474/downloads/20160012474.pdf'}]}
(OUT/'study.json').write_text(json.dumps(meta,indent=2)+'\n',encoding='utf-8')
plt.rcParams.update({'font.family':'DejaVu Sans','font.size':10,'axes.spines.top':False,'axes.spines.right':False})
fig,axs=plt.subplots(1,3,figsize=(13.5,4.8),constrained_layout=True);colors=['#393f94','#087f8c','#b42e8d']
for m,c in zip([1,5,20],colors):
    rr=[r for r in rows if r['mass_kg']==m and r['density_kg_m3']==1.225];axs[0].loglog([r['disk_area_m2'] for r in rr],[r['ideal_induced_power_W'] for r in rr],color=c,label=f'{m} kg')
for rho,c in zip([.9,1.225,1.4],colors):
    rr=[r for r in rows if r['mass_kg']==5 and r['density_kg_m3']==rho];axs[1].loglog([r['disk_area_m2'] for r in rr],[r['ideal_induced_power_W'] for r in rr],color=c,label=f'{rho} kg/m³')
rr=[r for r in rows if r['mass_kg']==5 and r['density_kg_m3']==1.225]
axs[2].semilogx([r['disk_area_m2'] for r in rr],[r['induced_disk_speed_m_s'] for r in rr],label='At the disk',color=colors[0]);axs[2].semilogx([r['disk_area_m2'] for r in rr],[r['far_wake_speed_m_s'] for r in rr],label='Far downstream',color=colors[2],ls='--')
for ax in axs:ax.set_xlabel('Swept disk area (m²)');ax.legend(fontsize=8);ax.grid(alpha=.2,which='both')
axs[0].set(title='A  |  More mass costs more power',ylabel='Ideal induced power (W)')
axs[1].set(title='B  |  Less dense air costs more power',ylabel='Ideal induced power (W)')
axs[2].set(title='C  |  Larger area moves air more slowly',ylabel='Downward air speed (m/s)')
fig.suptitle('Hovering means giving air downward momentum',fontsize=16,weight='bold')
fig.supxlabel('Ideal open-disk hover, no ground effect. A: density 1.225 kg/m³. B: mass 5 kg. C: mass 5 kg, density 1.225 kg/m³.',fontsize=9)
for ext in ['png','svg','pdf']:
    fig.savefig(OUT/f'hover-power.{ext}',dpi=180)
    if ext=='svg': describe_svg(OUT/'hover-power.svg','The ideal power cost of hovering','Three panels show how ideal open-disk induced power varies with supported mass and disk area, with a density comparison, and how disk and far-wake air speeds relate. The deterministic actuator-disk calculation assumes steady hover in still air and is not measured rotor performance.')
# A portrait companion preserves the three comparisons at phone width.
plt.rcParams.update({'font.size':14,'axes.labelsize':14,'axes.titlesize':14,'legend.fontsize':12})
fig,axs=plt.subplots(3,1,figsize=(5.5,12.5),constrained_layout=True)
for m,c in zip([1,5,20],colors):
    rr=[r for r in rows if r['mass_kg']==m and r['density_kg_m3']==1.225];axs[0].loglog([r['disk_area_m2'] for r in rr],[r['ideal_induced_power_W'] for r in rr],color=c,label=f'{m} kg')
for rho,c in zip([.9,1.225,1.4],colors):
    rr=[r for r in rows if r['mass_kg']==5 and r['density_kg_m3']==rho];axs[1].loglog([r['disk_area_m2'] for r in rr],[r['ideal_induced_power_W'] for r in rr],color=c,label=f'{rho} kg/m³')
rr=[r for r in rows if r['mass_kg']==5 and r['density_kg_m3']==1.225]
axs[2].semilogx([r['disk_area_m2'] for r in rr],[r['induced_disk_speed_m_s'] for r in rr],label='At the disk',color=colors[0]);axs[2].semilogx([r['disk_area_m2'] for r in rr],[r['far_wake_speed_m_s'] for r in rr],label='Far downstream',color=colors[2],ls='--')
for ax in axs:ax.set_xlabel('Swept disk area (m²)');ax.legend(fontsize=10);ax.grid(alpha=.2,which='both')
axs[0].set(title='A  |  More mass costs more power',ylabel='Ideal induced power (W)')
axs[1].set(title='B  |  Less dense air costs more power',ylabel='Ideal induced power (W)')
axs[2].set(title='C  |  Larger area moves air more slowly',ylabel='Downward air speed (m/s)')
fig.suptitle('Ideal hover: power and airflow',fontsize=17,weight='bold')
fig.supxlabel('Ideal open-disk hover, no ground effect. A: density 1.225 kg/m³. B: mass 5 kg. C: mass 5 kg, density 1.225 kg/m³.',fontsize=10)
for ext in ['png','svg','pdf']:
    fig.savefig(OUT/f'hover-power-mobile.{ext}',dpi=180)
    if ext=='svg':
        describe_svg(OUT/'hover-power-mobile.svg','The ideal power cost of hovering','Three stacked panels show how ideal open-disk induced power varies with supported mass and disk area, with a density comparison, and how disk and far-wake air speeds relate. The deterministic actuator-disk calculation assumes steady hover in still air and is not measured rotor performance.')
        overlay_visible_title(OUT/'hover-power-mobile.svg','Ideal hover: power and airflow',396,height=41)
print(json.dumps({'rows':len(rows),'baseline':meta['baseline'],'validation':meta['validation']}))
