"""Самостоятельная лабораторная работа курса 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)
os.chdir(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,
        job.get("previous", ""),
        job.get("dt", 0),
    )
    for name, kx, ky, q in job["materials"]:
        hi_addmaterial(name, kx, ky, q, job.get("cv", 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") and not job.get("skip_convection_balance"):
        check(
            stem + ": баланс конвективного теплоотвода",
            abs(convection - generated) / max(generated, 1e-30),
            0.005,
        )
    ho_close()
    hi_close()
    return row


JOB = {
    "polygon": [[0, 0], [10, 0], [10, 0.2], [0, 0.2]],
    "depth": 0.2,
    "cv": 3.45,
    "materials": [["Медь", 380, 380, 0]],
    "blocks": [[5, 0.1, "Медь", 1]],
    "boundaries": [["Холодные держатели", 0, 293.15, 0, 0]],
    "boundary_segments": [
        [0, 0.1, "Холодные держатели"],
        [10, 0.1, "Холодные держатели"],
        [5, 0, "<None>"],
        [5, 0.2, "<None>"],
    ],
    "samples": [[5, 0.1]],
}


def initial_state(job, mesh, series):
    initial = json.loads(json.dumps(job))
    initial["materials"] = [["Медь", 380, 380, 0]]
    initial["boundaries"] = [["Начальная температура", 0, 293.15, 0, 0]]
    length = initial["polygon"][1][0]
    height = initial["polygon"][2][1]
    initial["boundary_segments"] = [
        [0, height / 2, "Начальная температура"],
        [length, height / 2, "Начальная температура"],
        [length / 2, 0, "Начальная температура"],
        [length / 2, height, "Начальная температура"],
    ]
    initial["convection_edges"] = []
    result = heat(initial, mesh, "series-%d-step-000" % series)
    return result["model"].replace(".feh", ".anh")


area = 0.0002 * 0.0002
q = (12 / area) ** 2 / 5.8e7
length = 0.010
kappa = 380 / 3.45e6


def exact_center(time):
    series = 0
    for m in range(100):
        n = 2 * m + 1
        series += (
            (-1 if m % 2 else 1)
            * math.exp(-kappa * (n * math.pi / length) ** 2 * time)
            / (n**3)
        )
    return (
        293.15
        + q * length**2 / (8 * 380)
        - 4 * q * length**2 / (380 * math.pi**3) * series
    )


def run_series(mesh, dt, series):
    previous = initial_state(JOB, mesh, series)
    time = 0
    history = []
    maximum_error = 0
    step_number = 0
    while time < 0.2 - 1e-12:
        step = min(dt, 0.2 - time)
        job = json.loads(json.dumps(JOB))
        job["previous"] = previous
        job["dt"] = step
        job["materials"] = [["Медь", 380, 380, q]]
        step_number += 1
        result = heat(job, mesh, "series-%d-step-%03d" % (series, step_number))
        time += step
        center = result["samples"][0]["T_K"]
        reference = exact_center(time)
        maximum_error = max(maximum_error, abs(center - reference))
        adiabatic = 293.15 + q / 3.45e6 * time
        check(
            "Температура не выше адиабатической, серия %d шаг %d"
            % (series, step_number),
            max(0, center - adiabatic),
            1e-6,
        )
        history.append(
            {
                "time_s": time,
                "T_center_K": center,
                "analytic_1D_K": reference,
                "adiabatic_K": adiabatic,
                "I2t_A2_s": 12**2 * time,
            }
        )
        penultimate = previous
        previous = result["model"].replace(".feh", ".anh")
    return {
        "mesh_mm": mesh,
        "dt_s": dt,
        "model": result["model"],
        "previous_solution": penultimate,
        "maximum_analytic_error_K": maximum_error,
        "final_T_K": center,
        "I2t_A2_s": 12**2 * time,
        "history": history,
    }


for series, (mesh, dt) in enumerate(((0.2, 0.02), (0.1, 0.01), (0.05, 0.005)), 1):
    records.append(run_series(mesh, dt, series))
half = run_series(0.05, 0.0025, 4)
records[-1]["half_dt"] = half
check(
    "Уточнение только времени при неизменной сетке",
    half["maximum_analytic_error_K"] / records[-1]["maximum_analytic_error_K"],
    0.7,
)
check(
    "Окончательная погрешность относительно 1D ряда, К",
    half["maximum_analytic_error_K"],
    0.5,
)

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"]
