#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""F-3 메시 수렴 시험 — 같은 모델·같은 조건으로 메시만 3단계 바꿔 푼다.

왜 이 방식인가
  보고서 B-8 의 세 기록(770 → 866 → 946 MPa)은 메시만 다른 것이 아니라
  부품 수·껍질 수·용접 짝짓기가 전부 달랐다. 그래서 수렴 경향으로 읽을 수
  없었다. 여기서는 화면이 서버로 보낸 마지막 요청(last_request.json)을 그대로
  다시 쓴다 — 형상·고정점·하중점·재질이 한 글자도 다르지 않다.

무엇을 바꾸는가
  부품당 절점 상한 PER_CAP 과 총 절점 예산 budget_nodes 를 함께 올린다.
  PER_CAP 만 올리면 아무것도 안 바뀐다 — 부품당 배정은
  per = 총예산 × 그 부품 부피비 라서 작은 부품은 애초에 상한에 닿지 않고,
  실제 한도는 총예산 쪽이기 때문이다.

읽는 법
  변위 변화가 5% 이내면 '수렴'(변위는 구조 전체를 적분한 값이라 메시에 둔감).
  응력은 따로 본다 — 미분 성질이라 민감하고, 특이점 자리는 원래 수렴하지 않는다.
  ★ 부품 수·껍질 수·용접 쌍이 단계마다 다르면 '같은 모델' 이 아니므로
    수렴이라고 말하면 안 된다. 그래서 그 열도 표에 함께 적는다.
"""
import json
import os
import sys
import time

import fea_server as F

REQ = os.path.join(F.WORK_ROOT, 'last_request.json')
# (PER_CAP, 총 절점 예산)
STEPS = [(4000, 150000), (8000, 300000), (16000, 600000)]


def one(q, per_cap, budget):
    F.PER_CAP = per_cap
    t0 = time.time()
    r = F.run_fea(q.get('positions') or [],
                  load_N=float(q.get('load_N', 1000.0)),
                  material=q.get('material', F.DEF_MAT),
                  E=q.get('E'), nu=q.get('nu'),
                  fix_points=q.get('fix_points'),
                  load_points=q.get('load_points'),
                  pick_radius=q.get('pick_radius'),
                  weld_mode=q.get('weld_mode') or 'bond',
                  weld_throat=float(q.get('weld_throat') or 0.0),
                  part_sizes=q.get('part_sizes'),
                  part_names=q.get('part_names'),
                  budget_nodes=budget)
    res, m, sm = r['result'], r['mesh'], r['struct_mask']
    return {
        'per_cap': per_cap, 'budget': budget,
        'nodes': m['nodes'], 'elems': m['elements'],
        'parts': m['parts'], 'excluded': len(r.get('excluded_parts') or []),
        'hull': len(m.get('hull_parts') or []), 'welds': m.get('welds', 0),
        'through_wall': m.get('through_wall'),
        'disp': res['max_disp_mm'],
        'struct_MPa': res['max_vonmises_MPa'], 'sf': res['safety_factor'],
        'peak_MPa': res['peak_vonmises_MPa'],
        'kept_pct': sm['kept_pct'],
        'react_err': r.get('reaction_err_pct'),
        'applied_N': r.get('applied_N'), 'dropped_N': r.get('dropped_N'),
        'overlap': r.get('overlap_nodes'),
        'sec': round(time.time() - t0, 1), 'solve_sec': r['solve_sec'],
    }


def main():
    if not os.path.isfile(REQ):
        print('요청 기록이 없습니다: %s — 화면에서 전체해석을 한 번 돌리세요.' % REQ)
        return 1
    q = json.load(open(REQ, encoding='utf-8'))
    print('모델: 부품 %d개 / 고정점 %d / 하중점 %d / 재질 %s'
          % (len(q.get('part_names') or []), len(q.get('fix_points') or []),
             len(q.get('load_points') or []), q.get('material')))
    rows = []
    for per_cap, budget in STEPS:
        print('\n── PER_CAP %d / 예산 %d ──' % (per_cap, budget))
        try:
            row = one(q, per_cap, budget)
        except Exception as e:
            print('  실패: %s: %s' % (type(e).__name__, e))
            rows.append({'per_cap': per_cap, 'budget': budget, 'error': str(e)})
            continue
        rows.append(row)
        print('  절점 %s / 요소 %s / 벽당 %s겹 / 부품 %d(껍질 %d, 용접 %d)'
              % ('{:,}'.format(row['nodes']), '{:,}'.format(row['elems']),
                 row['through_wall'], row['parts'], row['hull'], row['welds']))
        print('  변위 %.4f mm / 구조최대 %.1f MPa(안전율 %.2f) / 첨두 %.1f MPa'
              % (row['disp'], row['struct_MPa'], row['sf'] or 0, row['peak_MPa']))
        print('  판정대상 %.1f%% / 반력오차 %s%% / %.1f초'
              % (row['kept_pct'],
                 'n/a' if row['react_err'] is None else '%.3f' % row['react_err'],
                 row['sec']))
    out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                       '_fea_수렴시험_결과.json')
    json.dump(rows, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
    print('\n기록: %s' % out)
    ok = [r for r in rows if 'error' not in r]
    if len(ok) >= 2:
        a, b = ok[-2], ok[-1]
        d = abs(b['disp'] - a['disp']) / max(a['disp'], 1e-9) * 100
        s = abs(b['struct_MPa'] - a['struct_MPa']) / max(a['struct_MPa'], 1e-9) * 100
        # ★ 용접 쌍 수까지 같아야 '같은 모델' 이다. 부품·껍질만 보면
        #   접합이 달라진 것을 놓쳐 수렴이라고 잘못 말하게 된다.
        same = (a['parts'] == b['parts'] and a['hull'] == b['hull']
                and a['welds'] == b['welds'])
        print('가장 촘촘한 두 단계 비교: 변위 %.2f%% / 구조최대 %.2f%% / 같은 모델 %s'
              % (d, s, '예' if same else '아니오(수렴 판단 불가)'))
    return 0


if __name__ == '__main__':
    sys.exit(main())
