"""Пространственный спектр в зазоре из сохранённых ANS: FEMM 5 MicroPython.

TG_GAP_RESULT_FILE — абсолютный result.json завершённого этапа15/all.
TG_GAP_OUTPUT_ROOT — существующий каталог для нового gap-calculated-NN.
TG_GAP_SCRIPT_FILE — абсолютный путь к этому скрипту при запуске через exec.
По умолчанию читаются healthy/eccentric, положение0, 180точек на R=71мм.
Поле не решается, исходные FEM/ANS не сохраняются и контролируются SHA-256.
Это спектр по пространственному углу φ при фиксированном времени, не по t.
"""
import os
import sys
import math
import json
import hashlib
import binascii

saved=globals().get('__file__','') or (sys.argv[0] if sys.argv else '')
SCRIPT=globals().get('TG_GAP_SCRIPT_FILE',os.path.abspath(saved))
INPUT=globals().get('TG_GAP_RESULT_FILE','')
if not INPUT:
    raise ValueError('Задайте TG_GAP_RESULT_FILE: result.json завершённого этапа15/all')
SOURCE=os.path.dirname(os.path.abspath(INPUT))
DEST=globals().get('TG_GAP_OUTPUT_ROOT',SOURCE)
ENGINE=globals().get('TG_ENGINE_FILE','C:/Program Files/femm-qt/bin/femm_headless.exe')
POSITION=int(globals().get('TG_GAP_POSITION_INDEX',0))
COUNT=180
HMAX=15
PI=math.pi


def sha(path):
    h=hashlib.sha256()
    with open(path,'rb') as f:
        while True:
            data=f.read(65536)
            if not data:
                break
            h.update(data)
    return binascii.hexlify(h.digest()).decode()


def real(value):
    return float(complex(value).real)


def spectrum(values):
    # Для равномерных φk=2πk/N ортогональность синусов/косинусов даёт 2/N.
    # Амплитуда h = sqrt(ah²+bh²), единица Тл. Множителя hω здесь нет:
    # это пространственный B(φ), а не временная ЭДС из производной Ψ(t).
    rows=[]
    for h in range(1,HMAX+1):
        a=2*sum(v*math.cos(h*2*PI*k/COUNT) for k,v in enumerate(values))/COUNT
        b=2*sum(v*math.sin(h*2*PI*k/COUNT) for k,v in enumerate(values))/COUNT
        rows.append({'harmonic':h,'cos_T':a,'sin_T':b,'amplitude_T':math.sqrt(a*a+b*b)})
    return rows


input_sha=sha(INPUT)
with open(INPUT) as f:
    original=json.load(f)
if original.get('geometry_revision')!='open-slot-wedges-v2' or '15' not in original.get('results',{}):
    raise ValueError('Нужен завершённый этап15/all геометрии open-slot-wedges-v2')
cfg=original['parameters']
radius=(cfg['rotor_radius_mm']+cfg['bore_radius_mm'])/2
if not cfg['rotor_radius_mm']+abs(cfg['eccentricity_mm'])<radius<cfg['bore_radius_mm']:
    raise ValueError('Измерительная окружность должна целиком лежать в воздушном зазоре')
index=1
while os.path.exists(DEST+'/gap-calculated-%02d'%index):
    index+=1
OUT=DEST+'/gap-calculated-%02d'%index
os.mkdir(OUT)
records=[]
for kind in ['healthy','eccentric']:
    samples=original['results']['15'][kind]['samples']
    if POSITION<0 or POSITION>=len(samples):
        raise ValueError('TG_GAP_POSITION_INDEX вне диапазона сохранённых положений')
    source=samples[POSITION]
    model=SOURCE+'/'+source['model']
    solution=SOURCE+'/'+source['solution']
    before={'model':sha(model),'solution':sha(solution)}
    for key in ['model','solution']:
        expected=source.get(key+'_sha256')
        if expected and expected!=before[key]:
            raise ValueError('SHA-256 опубликованного поля не совпал: '+key)
    opendocument(model)
    mi_loadsolution()
    mo_smooth('off')
    points=[]
    try:
        for k in range(COUNT):
            phi=2*PI*k/COUNT
            x,y=radius*math.cos(phi),radius*math.sin(phi)
            p=mo_getpointvalues(x,y)
            bx,by=real(p[1]),real(p[2])
            # Поворот неподвижного Cartesian B в локальный базис окружности.
            br=bx*math.cos(phi)+by*math.sin(phi)
            bt=-bx*math.sin(phi)+by*math.cos(phi)
            points.append({'spatial_angle_deg':k*360/COUNT,'point_mm':[x,y],
                           'Bx_T':bx,'By_T':by,'Br_T':br,'Bt_T':bt,
                           'relative_permeability':[real(p[9]),real(p[10])]})
    finally:
        mo_close()
        mi_close()
    after={'model':sha(model),'solution':sha(solution)}
    if before!=after:
        raise RuntimeError('Чтение изменило входную FEM/ANS')
    br_values=[p['Br_T'] for p in points]
    bt_values=[p['Bt_T'] for p in points]
    records.append({'kind':kind,'position_index':POSITION,
                    'rotor_angle_deg':source['rotor_angle_deg'],
                    'eccentricity_mm':source['eccentricity_mm'],
                    'physical_phase_currents_A':source['physical_phase_currents_A'],
                    'excitation_A':source['excitation_A'],
                    'source_model':source['model'],'source_solution':source['solution'],
                    'source_model_sha256':before['model'],'source_solution_sha256':before['solution'],
                    'inputs_unchanged':True,'samples':points,
                    'Br_mean_T':sum(br_values)/COUNT,'Bt_mean_T':sum(bt_values)/COUNT,
                    'Br_harmonics':spectrum(br_values),'Bt_harmonics':spectrum(bt_values)})
    print('SPATIAL_GAP',kind,'Br_h2_T',records[-1]['Br_harmonics'][1]['amplitude_T'])
checks=[{'name':'Все180точек каждой окружности лежат в немагнитном зазоре',
         'passed':all(abs(v-1)<1e-6 for r in records for p in r['samples'] for v in p['relative_permeability'])},
        {'name':'Все отсчёты B конечны','passed':all(math.isfinite(p['Br_T']) and math.isfinite(p['Bt_T']) for r in records for p in r['samples'])},
        {'name':'ИсходныеFEM/ANS/resultнеизменны','passed':all(r['inputs_unchanged'] for r in records) and sha(INPUT)==input_sha}]
result={'schema':'femm5-saved-spatial-gap-v1','geometry_revision':'open-slot-wedges-v2',
        'runtime':sys.version,'implementation':str(sys.implementation),
        'script_sha256':sha(SCRIPT),'engine_file_sha256':sha(ENGINE),
        'input_result_sha256':input_sha,'input_result_file':os.path.abspath(INPUT),
        'ring_center_mm':[0,0],'ring_radius_mm':radius,'spatial_samples':COUNT,
        'point_field_smoothing':'off; elementwise native B',
        'exported_harmonic_max':HMAX,'spatial_nyquist_order':COUNT//2,
        'records':records,'checks':checks,'passed':all(c['passed'] for c in checks),
        'field_rerun':False,'solver_called':False,
        'scope':'Нативное чтение двух сохранённых магнитостатических полей в одном положении. Пространственный B(φ) по окружности зазора, не временной спектр Ψ(t) и не внешнее поле. Ростh2 не является критериемPASS, универсальным диагнозом или проверкой сходимости высоких гармоник.'}
with open(OUT+'/spatial-gap.json','w') as f:
    json.dump(result,f)
with open(OUT+'/spatial-gap-samples.csv','w') as f:
    f.write('kind,spatial_angle_deg,x_mm,y_mm,Bx_T,By_T,Br_T,Bt_T\n')
    for r in records:
        for p in r['samples']:
            row=[r['kind'],p['spatial_angle_deg']]+p['point_mm']+[p['Bx_T'],p['By_T'],p['Br_T'],p['Bt_T']]
            f.write(','.join(str(v) for v in row)+'\n')
with open(OUT+'/spatial-gap-harmonics.csv','w') as f:
    f.write('kind,component,harmonic,cos_T,sin_T,amplitude_T\n')
    for r in records:
        for component in ['Br','Bt']:
            for h in r[component+'_harmonics']:
                row=[r['kind'],component,h['harmonic'],h['cos_T'],h['sin_T'],h['amplitude_T']]
                f.write(','.join(str(v) for v in row)+'\n')
print('SPATIAL_GAP_RESULT',OUT,result['passed'])
if not result['passed']:
    raise RuntimeError('Пространственная постобработка не прошла проверки')
