"""Самостоятельная лабораторная работа курса FEMM 5.
Запуск: встроенный MicroPython FEMM 5, команды FEMM доступны непосредственно.
Геометрические координаты и глубина — мм; результаты интегралов — единицы СИ.
Данные, допущения, аналитические ориентиры и ход исследования заданы в README.md.
Каждая серия записывается в новую calculated-XX; прежние результаты сохраняются.
Сохранённая модель закрывается и открывается до расчёта, включая русские имена.
"""

import os
import sys
import json
import math

script = globals().get("__file__", "") or (sys.argv[0] if sys.argv else "")
base = os.path.dirname(os.path.abspath(script)) if script else os.getcwd()
index = 1
while "calculated-%02d" % index in os.listdir(base):
    index += 1
directory = base + "/calculated-%02d" % index
os.mkdir(directory)
records = []
checks = []


def real(value):
    if isinstance(value, (tuple, list)):
        return real(value[0])
    return complex(value).real


def check(name, value, limit):
    checks.append(
        {
            "name": name,
            "value": value,
            "limit": limit,
            "passed": math.isfinite(value) and value <= limit,
        }
    )


def polygon(prefix, points):
    for x, y in points:
        globals()[prefix + "_addnode"](x, y)
    for i, (x, y) in enumerate(points):
        x2, y2 = points[(i + 1) % len(points)]
        globals()[prefix + "_addsegment"](x, y, x2, y2)


def geometry(prefix, job, mesh):
    if job.get("polygon"):
        polygon(prefix, job["polygon"])
    for x1, y1, x2, y2 in job.get("interfaces", []):
        globals()[prefix + "_addnode"](x1, y1)
        globals()[prefix + "_addnode"](x2, y2)
        globals()[prefix + "_addsegment"](x1, y1, x2, y2)
    for x, y, radius, name in job.get("circles", []):
        globals()[prefix + "_addnode"](x - radius, y)
        globals()[prefix + "_addnode"](x + radius, y)
        globals()[prefix + "_addarc"](x - radius, y, x + radius, y, 180, 1)
        globals()[prefix + "_addarc"](x + radius, y, x - radius, y, 180, 1)
        globals()[prefix + "_selectarcsegment"](x, y + radius)
        globals()[prefix + "_selectarcsegment"](x, y - radius)
        if prefix == "ci":
            ci_setarcsegmentprop(1, "<None>", 0, 0, name or "<None>")
        else:
            hi_setarcsegmentprop(1, name or "<None>", 0, 0, "<None>")
        globals()[prefix + "_clearselected"]()
    for x, y, name, group in job["blocks"]:
        globals()[prefix + "_addblocklabel"](x, y)
        globals()[prefix + "_selectlabel"](x, y)
        globals()[prefix + "_setblockprop"](name, 0, mesh, group)
        globals()[prefix + "_clearselected"]()


def current(job, mesh, stem):
    newdocument(3)
    ci_probdef("millimeters", "planar", 0, 1e-10, job["depth"], 30)
    for name, sx, sy in job["materials"]:
        ci_addmaterial(name, sx, sy, 1, 1, 0, 0)
    for name, voltage, amperes, kind in job["terminals"]:
        ci_addconductorprop(name, voltage, amperes, kind)
    geometry("ci", job, mesh)
    for x, y, name in job.get("electrode_segments", []):
        ci_selectsegment(x, y)
        ci_setsegmentprop("<None>", mesh, 0, 0, 0, name)
        ci_clearselected()
    path = directory + "/" + stem + ".fec"
    ci_saveas(path)
    ci_close()
    opendocument(path)
    ci_analyze(1)
    ci_loadsolution()
    co_smooth("off")
    terminals = {}
    terminal_power = 0
    balance = 0
    current_scale = 0
    for name, voltage, amperes, kind in job["terminals"]:
        u, i = co_getconductorproperties(name)
        terminals[name] = {"U_V": real(u), "I_A": real(i)}
        terminal_power += real(complex(u) * complex(i).conjugate())
        balance += complex(i)
        current_scale = max(current_scale, abs(complex(i)))
    powers = {}
    power = 0
    for x, y, name, group in job["blocks"]:
        if name == "<No Mesh>":
            continue
        co_groupselectblock(group)
        powers[str(group)] = real(co_blockintegral(0))
        power += powers[str(group)]
        co_clearblock()
    samples = []
    for x, y in job.get("samples", []):
        values = co_getpointvalues(x, y)
        samples.append(
            {
                "x_mm": x,
                "y_mm": y,
                "U_V": real(values[0]),
                "J_A_m2": math.sqrt(
                    abs(complex(values[1])) ** 2 + abs(complex(values[2])) ** 2
                ),
            }
        )
    row = {
        "model": stem + ".fec",
        "mesh_mm": mesh,
        "nodes": co_numnodes(),
        "elements": co_numelements(),
        "terminals": terminals,
        "P_W": power,
        "group_power_W": powers,
        "samples": samples,
        "current_balance_relative": abs(balance) / current_scale,
        "power_balance_relative": abs(power - terminal_power) / max(power, 1e-30),
    }
    check(stem + ": баланс токов", row["current_balance_relative"], 1e-6)
    check(stem + ": баланс мощности", row["power_balance_relative"], 1e-6)
    co_close()
    ci_close()
    return row


def heat(job, mesh, stem):
    newdocument(2)
    hi_probdef("millimeters", "planar", 1e-10, job["depth"], 30)
    for name, kx, ky, q in job["materials"]:
        hi_addmaterial(name, kx, ky, q, 0)
    for name, kind, temp, ambient, h in job["boundaries"]:
        hi_addboundprop(name, kind, temp, 0, ambient, h, 0)
    geometry("hi", job, mesh)
    for x, y, name in job.get("boundary_segments", []):
        hi_selectsegment(x, y)
        hi_setsegmentprop(name, mesh, 0, 0, 0, "<None>")
        hi_clearselected()
    path = directory + "/" + stem + ".feh"
    hi_saveas(path)
    hi_close()
    opendocument(path)
    hi_analyze(1)
    hi_loadsolution()
    ho_smooth("off")
    averages = {}
    generated = 0
    for x, y, name, group in job["blocks"]:
        ho_groupselectblock(group)
        averages[str(group)] = real(ho_blockintegral(0))
        volume = real(ho_blockintegral(2))
        q = [m[3] for m in job["materials"] if m[0] == name][0]
        generated += q * volume
        ho_clearblock()
    samples = []
    for x, y in job.get("samples", []):
        samples.append({"x_mm": x, "y_mm": y, "T_K": real(ho_getpointvalues(x, y)[0])})
    convection = 0
    for x1, y1, x2, y2, ambient, h in job.get("convection_edges", []):
        count = 200
        total = 0
        for n in range(count + 1):
            t = real(
                ho_getpointvalues(
                    x1 + (x2 - x1) * n / count, y1 + (y2 - y1) * n / count
                )[0]
            )
            total += (0.5 if n in (0, count) else 1) * (t - ambient)
        length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) * 0.001
        convection += total / count * length * job["depth"] * 0.001 * h
    row = {
        "model": stem + ".feh",
        "mesh_mm": mesh,
        "nodes": ho_numnodes(),
        "elements": ho_numelements(),
        "samples": samples,
        "group_average_K": averages,
        "generated_W": generated,
        "convection_W": convection,
    }
    if job.get("convection_edges"):
        check(
            stem + ": баланс конвективного теплоотвода",
            abs(convection - generated) / max(generated, 1e-30),
            0.005,
        )
    ho_close()
    hi_close()
    return row


JOB = json.loads(
    '{"polygon": [[0, 0], [100, 0], [100, 10], [0, 10]], "depth": 1, "materials": [["Медь", 58000000.0, 58000000.0], ["Резистивный контакт", 1000000.0, 1000000.0]], "blocks": [[20, 5, "Медь", 1], [50, 5, "Резистивный контакт", 2], [80, 5, "Медь", 3]], "terminals": [["Питание", 0.02, 0, 1], ["Возврат", 0, 0, 1]], "electrode_segments": [[0, 5, "Питание"], [100, 5, "Возврат"]], "samples": [[50, 5]], "interfaces": [[45, 0, 45, 10], [55, 0, 55, 10]]}'
)


def analytic_center(sources):
    lengths = (0.045, 0.010, 0.045)
    ks = (380, 1, 380)
    previous = 0
    integral = 0
    resistance = 0
    for length, k, q in zip(lengths, ks, sources):
        integral += (previous * length + q * length**2 / 2) / k
        resistance += length / k
        previous += q * length
    flux = -integral / resistance
    temperature = 293.15
    position = 0
    for length, k, q in zip(lengths, ks, sources):
        distance = min(length, 0.05 - position)
        temperature -= flux * distance / k + q * distance**2 / (2 * k)
        if position + length >= 0.05:
            return temperature
        flux += q * length
        position += length


for level, mesh in enumerate((0.5, 0.25, 0.125)):
    temperatures = [293.15, 293.15]
    history = []
    for iteration in range(1, 41):
        sigma_cu = 5.8e7 / (1 + 0.00393 * (temperatures[0] - 293.15))
        sigma_contact = 1e6 / (1 + 0.004 * (temperatures[1] - 293.15))
        electrical = json.loads(json.dumps(JOB))
        electrical["materials"] = [
            ["Медь", sigma_cu, sigma_cu],
            ["Резистивный контакт", sigma_contact, sigma_contact],
        ]
        stem = "level-%d-iteration-%02d" % (level + 1, iteration)
        electric = current(electrical, mesh, stem + "-current")
        powers = [electric["group_power_W"][str(n)] for n in (1, 2, 3)]
        volumes = (0.045 * 0.01 * 0.001, 0.01 * 0.01 * 0.001, 0.045 * 0.01 * 0.001)
        sources = [p / v for p, v in zip(powers, volumes)]
        thermal = {
            "polygon": JOB["polygon"],
            "interfaces": JOB["interfaces"],
            "depth": 1,
            "materials": [
                ["Левая медь", 380, 380, sources[0]],
                ["Контакт", 1, 1, sources[1]],
                ["Правая медь", 380, 380, sources[2]],
            ],
            "blocks": [
                [20, 5, "Левая медь", 1],
                [50, 5, "Контакт", 2],
                [80, 5, "Правая медь", 3],
            ],
            "boundaries": [["Термостат", 0, 293.15, 0, 0]],
            "boundary_segments": [[0, 5, "Термостат"], [100, 5, "Термостат"]],
            "samples": [[50, 5]],
        }
        thermal_result = heat(thermal, mesh, stem + "-heat")
        new_temperatures = [
            (
                thermal_result["group_average_K"]["1"]
                + thermal_result["group_average_K"]["3"]
            )
            / 2,
            thermal_result["group_average_K"]["2"],
        ]
        residual = max(abs(a - b) for a, b in zip(new_temperatures, temperatures))
        history.append(
            {
                "iteration": iteration,
                "residual_K": residual,
                "I_A": abs(electric["terminals"]["Питание"]["I_A"]),
                "T_contact_K": new_temperatures[1],
            }
        )
        if residual < 0.001:
            break
        temperatures = [(a + b) / 2 for a, b in zip(new_temperatures, temperatures)]
    check("Нелинейная сходимость, сетка %d" % (level + 1), residual, 0.001)
    reference = (0.09 / sigma_cu + 0.01 / sigma_contact) / (0.01 * 0.001)
    numerical = 0.02 / abs(electric["terminals"]["Питание"]["I_A"])
    check(
        "Независимая последовательная электрическая схема",
        abs(numerical / reference - 1),
        1e-6,
    )
    center = thermal_result["samples"][0]["T_K"]
    check(
        "Независимое кусочно-одномерное тепловое решение, К",
        abs(center - analytic_center(sources)),
        0.15,
    )
    check(
        "Передача джоулевой мощности в тепловую задачу",
        abs(thermal_result["generated_W"] / electric["P_W"] - 1),
        1e-6,
    )
    records.append(
        {
            "mesh_mm": mesh,
            "iterations": iteration,
            "residual_K": residual,
            "I_A": history[-1]["I_A"],
            "R_ohm": numerical,
            "T_center_K": center,
            "P_W": electric["P_W"],
            "history": history,
            "model": thermal_result["model"],
            "electrical_model": electric["model"],
        }
    )
check(
    "Сходимость температуры по сетке, К",
    abs(records[-1]["T_center_K"] - records[-2]["T_center_K"]),
    0.15,
)

result = {
    "runtime": "FEMM 5; embedded MicroPython",
    "records": records,
    "checks": checks,
    "passed": all(item["passed"] for item in checks),
}
with open(directory + "/result.json", "w") as output:
    json.dump(result, output)
print(json.dumps(result))
assert result["passed"]
