"""Новый учебный чашечный электромагнит. Только встроенный Python FEMM 5.
Размеры геометрии заданы в мм, результаты — в СИ. Это собственная
осесимметричная модель, а не копия библиотечного FEM-файла.
"""
import math
import os

MU0 = 4e-7 * math.pi
PARAMETERS = {
    'outer_radius_mm': 25.0, 'height_mm': 30.0, 'base_mm': 5.0,
    'armature_mm': 5.0, 'gap_mm': 1.0, 'turns': 400, 'current_A': 1.5,
    'fill': 0.55, 'coil_height_mm': 23.0, 'rho_copper_ohm_m': 1.7241e-8,
    'density_copper_kg_m3': 8960.0, 'density_steel_kg_m3': 7850.0,
    'required_force_N': 50.0, 'max_J_A_mm2': 6.0, 'max_B_sample_T': 1.65,
    'r_bounds_mm': [6.0, 13.0], 't_bounds_mm': [3.0, 7.0],
}
# Открыто заданная учебная кривая; это не паспорт конкретной марки стали.
BH = [(0.0, 0.0), (0.25, 100.0), (0.5, 200.0), (0.9, 400.0),
      (1.2, 800.0), (1.45, 2000.0), (1.6, 5000.0), (1.7, 10000.0),
      (1.85, 40000.0), (2.0, 160000.0)]


def label(x, z, material, mesh, group=0, circuit='<None>', turns=0):
    mi_addblocklabel(x, z)
    mi_selectlabel(x, z)
    mi_setblockprop(material, 0, mesh, circuit, 0, group, turns)
    mi_clearselected()


def poly(points):
    for x, z in points:
        mi_addnode(x, z)
    for a, b in zip(points, points[1:] + [points[0]]):
        mi_addsegment(a[0], a[1], b[0], b[1])


def field(r, t, folder, stem, mesh=1.0, outer=100.0, gap=None):
    """Каждый вызов строит новую геометрию, сетку и нелинейное решение."""
    p = PARAMETERS
    R, H, b, a = p['outer_radius_mm'], p['height_mm'], p['base_mm'], p['armature_mm']
    g = p['gap_mm'] if gap is None else gap
    w = R - t - r - 2.0
    h = p['coil_height_mm']
    assert r > 0 and t > 0 and w > 0 and g > 0
    newdocument(0)
    mi_probdef(0, 'millimeters', 'axi', 1e-9, 1, 28)
    mi_addmaterial('Воздух', 1, 1, 0, 0, 0)
    mi_addmaterial('Обмотка — немагнитная область', 1, 1, 0, 0, 0)
    mi_addmaterial('Сталь — учебная нелинейная кривая', 1, 1, 0, 0, 0)
    for B, hfield in BH:
        mi_addbhpoint('Сталь — учебная нелинейная кривая', B, hfield)
    mi_addcircprop('Обмотка возбуждения', p['current_A'], 1)
    mi_addboundprop('Внешняя граница: A = 0', 0, 0, 0, 0, 0, 0, 0, 0, 0)

    # Единое тело ярма; внутреннее окно образовано самим многоугольником.
    poly([(0, 0), (R, 0), (R, H), (R-t, H), (R-t, b),
          (r, b), (r, H), (0, H)])
    poly([(0, H+g), (R, H+g), (R, H+g+a), (0, H+g+a)])
    poly([(r+1, b+1), (R-t-1, b+1), (R-t-1, b+1+h), (r+1, b+1+h)])
    label(r/2, H/2, 'Сталь — учебная нелинейная кривая', mesh, 1)
    label(R/2, H+g+a/2, 'Сталь — учебная нелинейная кривая', mesh, 2)
    label((r+R-t)/2, b+1+h/2, 'Обмотка — немагнитная область', mesh, 3,
          'Обмотка возбуждения', p['turns'])
    # Явная конечная полукруглая граница позволяет независимо менять область.
    zc = H/2
    mi_addnode(0, zc-outer); mi_addnode(0, zc+outer)
    # Узлы на оси уже принадлежат железу; соединяем только внешние интервалы.
    mi_addsegment(0, zc-outer, 0, 0)
    mi_addsegment(0, H, 0, H+g)
    mi_addsegment(0, H+g+a, 0, zc+outer)
    mi_addarc(0, zc-outer, 0, zc+outer, 180, 3)
    mi_selectarcsegment(outer, zc)
    mi_setarcsegmentprop(3, 'Внешняя граница: A = 0', 0, 0)
    mi_clearselected()
    label(R+8, H/2, 'Воздух', 3*mesh, 0)
    # Отдельная метка зазора связывается с общей областью воздуха вокруг ярма.
    # Для зазора задаём размеры его граничных отрезков, а не лишнюю метку.
    for x, z in [(r/2, H), (R-t/2, H), (R/2, H+g)]:
        mi_selectsegment(x, z)
        mi_setsegmentprop('<None>', 0.2*mesh, 0, 0, 0)
        mi_clearselected()
    path = os.path.join(folder, stem + '.fem')
    mi_saveas(path)
    mi_analyze(1)
    mi_loadsolution()
    mo_smooth('off')
    mo_clearblock(); mo_groupselectblock(2)
    signed_force = float(mo_blockintegral(19))
    mo_clearblock()
    # Проверка энергии относится ко всему полю, включая воздух.
    for group in [0, 1, 2, 3]:
        mo_groupselectblock(group)
    energy = float(mo_blockintegral(2))
    coenergy = float(mo_blockintegral(17))
    mo_clearblock()
    circuit = mo_getcircuitproperties('Обмотка возбуждения')
    linkage = float(circuit[2])
    # Максимум в конечном наборе внутренних точек. Не выдаём его за sup|B|.
    samples = []
    for x1, z1, x2, z2 in [(0.5, 0.5, R-0.5, b-0.5),
                          (0.5, b+0.5, r-0.5, H-0.5),
                          (R-t+0.5, b+0.5, R-0.5, H-0.5),
                          (0.5, H+g+0.5, R-0.5, H+g+a-0.5)]:
        for u in [0.2, 0.5, 0.8]:
            for v in [0.2, 0.5, 0.8]:
                x, z = x1 + u*(x2-x1), z1 + v*(z2-z1)
                q = mo_getpointvalues(x, z)
                B = math.sqrt(float(q[1])**2 + float(q[2])**2)
                samples.append({'r_mm': x, 'z_mm': z, 'B_T': B})
    max_B = max(s['B_T'] for s in samples)
    elems = mo_numelements()
    mo_close(); mi_close()
    # Согласованная медная модель: постоянный N, заданный коэффициент заполнения.
    A_wire = p['fill'] * w * h / p['turns']  # мм²
    mean_radius = (r + R-t)/2 * 1e-3
    length = p['turns'] * 2*math.pi*mean_radius
    resistance = p['rho_copper_ohm_m'] * length / (A_wire*1e-6)
    current = p['current_A']
    volume_copper = length * A_wire*1e-6
    volume_steel = math.pi*(R**2*b + r*r*(H-b) +
                         (R**2-(R-t)**2)*(H-b) + R**2*a)*1e-9
    row = {'r_mm': r, 't_mm': t, 'gap_mm': g, 'mesh_scale': mesh,
           'outer_radius_mm': outer, 'elements': elems,
           'force_N': -signed_force, 'force_signed_z_N': signed_force,
           'power_copper_W': current**2*resistance,
           'resistance_20C_ohm': resistance, 'wire_length_m': length,
           'wire_area_mm2': A_wire, 'coil_width_mm': w,
           'J_A_mm2': current/A_wire, 'B_sample_max_T': max_B,
           'mass_copper_kg': p['density_copper_kg_m3']*volume_copper,
           'mass_steel_kg': p['density_steel_kg_m3']*volume_steel,
           'energy_J': energy, 'coenergy_J': coenergy,
           'flux_linkage_Wb_turn': linkage,
           'energy_identity_relative': abs(energy+coenergy-current*linkage)/max(1e-12, abs(current*linkage)),
           'model': stem+'.fem', 'solution': stem+'.ans', 'B_samples': samples}
    row['constraints'] = {
        'force': (p['required_force_N']-row['force_N'])/p['required_force_N'],
        'current_density': row['J_A_mm2']/p['max_J_A_mm2']-1,
        'induction_samples': max_B/p['max_B_sample_T']-1,
        'coil_window': (2.0-w)/2.0,
    }
    row['violation'] = sum(max(0.0, c)**2 for c in row['constraints'].values())
    row['feasible'] = max(row['constraints'].values()) <= 0
    return row
