"""Hypothetical freely suspended charged spheres: force balance, not dust removal."""
import math,csv,json
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
ROOT=Path(__file__).resolve().parent
RHO=2500.;G=9.80665;FIELD=10000.;CHARGES=[-10.,-1.,0.,1.,10.]
def model(radius_um,charge_fC,field=FIELD):
    radius=radius_um*1e-6;charge=charge_fC*1e-15
    volume=4*math.pi*radius**3/3;mass=RHO*volume;weight=mass*G;electric=charge*field
    return dict(radius_um=radius_um,charge_fC=charge_fC,field_upward_V_per_m=field,density_kg_per_m3=RHO,mass_kg=mass,weight_downward_N=weight,electric_upward_N=electric,net_upward_N=electric-weight,net_acceleration_upward_m_per_s2=(electric-weight)/mass,signed_electric_to_weight=electric/weight,balance_charge_fC=weight/field/1e-15)
rows=[model(i/2,q) for q in CHARGES for i in range(2,101)]
with (ROOT/'force-weight.csv').open('w',newline='',encoding='utf-8') as f:
    w=csv.DictWriter(f,fieldnames=rows[0]);w.writeheader();w.writerows(rows)
checks=[]
for r in rows:
    # Independent volume from diameter and electric acceleration q/m times E.
    mass=RHO*math.pi*(r['radius_um']*2e-6)**3/6
    accel=r['charge_fC']*1e-15/mass*FIELD-G
    assert math.isclose(mass,r['mass_kg'],rel_tol=1e-14)
    assert math.isclose(accel,r['net_acceleration_upward_m_per_s2'],rel_tol=1e-12,abs_tol=1e-10)
for radius in (1,10,50):
    p=model(radius,1);n=model(radius,-1);zero=model(radius,0);rev=model(radius,1,-FIELD)
    assert p['electric_upward_N']==-n['electric_upward_N']==-rev['electric_upward_N']
    assert math.isclose(zero['net_acceleration_upward_m_per_s2'],-G)
    assert abs(model(radius,p['balance_charge_fC'])['net_acceleration_upward_m_per_s2'])<1e-12
    assert math.isclose(model(radius*2,1)['mass_kg']/p['mass_kg'],8)
    checks.append(dict(radius_um=radius,balance_charge_fC=p['balance_charge_fC'],polarity_zero_balance_and_cubic_scaling=True))
meta=dict(title='A field can oppose or reinforce gravity on a free charged grain',kind='Hypothetical deterministic force balance; no measured dust or device',parameters=dict(density_kg_per_m3=RHO,gravity_m_per_s2=G,uniform_upward_field_V_per_m=FIELD,charges_fC=CHARGES,radius_range_um=[1,50],radius_step_um=.5),equations=dict(mass='rho*(4*pi/3)*r^3',electric_upward='q*E',gravity_downward='m*g',net_upward='q*E-m*g',acceleration_upward='q*E/m-g'),assumptions=['Upward is positive; gravitational acceleration is downward.','Charge values are prescribed independently of radius: not a charging model or empirical grain distribution.','Free spherical grain, homogeneous constant density, uniform static field and constant charge.','No adhesion, contact, drag, buoyancy, polarization, image-charge interaction, collisions, charge exchange or field nonuniformity.','Instantaneous force balance only: no trajectories, clearing fraction or practical levitation stability.','No hardware, electrode spacing, voltage or construction prescription.','Neutral grains have zero qE in this model; real polarized particles may respond in nonuniform fields.','A positive force-to-weight ratio exceeding one means upward initial acceleration only under these assumptions; it does not establish surface removal.'],rows=len(rows),sources=[dict(url='https://ntrs.nasa.gov/citations/20150016082',scope='Independent EDS comparator uses travelling nonuniform fields and electrostatic/dielectrophoretic forces; not validation of this uniform-field model'),dict(url='https://openstax.org/books/university-physics-volume-2/pages/5-4-electric-field',scope='Electric force on a test charge and electric field definition')],checked_date='2026-09-27')
(ROOT/'metadata.json').write_text(json.dumps(meta,indent=2)+'\n')
(ROOT/'checks.json').write_text(json.dumps(dict(all_pass=True,rows_checked=len(rows),checks=checks),indent=2)+'\n')
plt.rcParams.update({'font.family':'DejaVu Sans','font.size':10,'axes.spines.top':False,'axes.spines.right':False})
fig,ax=plt.subplots(1,2,figsize=(10.5,4.3),layout='constrained')
colors=['#533b94','#a65fa5','#888888','#07898d','#2e5fc6']
for q,c in zip(CHARGES,colors):
    rr=[r for r in rows if r['charge_fC']==q]
    ax[0].plot([r['radius_um'] for r in rr],[r['signed_electric_to_weight'] for r in rr],color=c,label=f'{q:+g} fC')
ax[0].set(xlabel='Grain radius (µm)',ylabel='Signed electric force / weight',yscale='symlog',title='A. Charge polarity changes the direction')
ax[0].axhline(1,color='black',ls=':',lw=1);ax[0].text(27,1.5,'+1: electric force balances weight',fontsize=8);ax[0].legend(title='Prescribed charge',fontsize=8,ncol=2)
rr=[r for r in rows if r['charge_fC']==1]
ax[1].loglog([r['radius_um'] for r in rr],[r['balance_charge_fC'] for r in rr],color='#2e5fc6');ax[1].set(xlabel='Grain radius (µm)',ylabel='Positive charge to balance weight (fC)',title='B. Required charge grows with volume')
for a in ax:a.grid(alpha=.2)
fig.suptitle('Free charged spheres in a uniform upward field • hypothetical inputs',fontsize=12)
fig.supxlabel('E = 10,000 V/m; density = 2,500 kg/m³. No adhesion, drag or charging model; not a dust-clearing prediction.',fontsize=9)
for ext in ('png','svg','pdf'):fig.savefig(ROOT/f'free-grain-forces.{ext}',dpi=200,bbox_inches='tight')
print(json.dumps(dict(rows=len(rows),example_10um_1fC=model(10,1),all_checks_pass=True)))
