"""Самостоятельная лабораторная работа курса 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, 1], [0, 1]],
    "depth": 100,
    "cv": 3.45,
    "materials": [["Медь", 380, 380, 100000000.0]],
    "blocks": [[5, 0.5, "Медь", 1]],
    "boundaries": [["Конвекция", 2, 0, 293.15, 1000]],
    "boundary_segments": [
        [5, 0, "Конвекция"],
        [5, 1, "Конвекция"],
        [0, 0.5, "<None>"],
        [10, 0.5, "<None>"],
    ],
    "convection_edges": [[0, 0, 10, 0, 293.15, 1000], [0, 1, 10, 1, 293.15, 1000]],
    "skip_convection_balance": True,
    "samples": [[5, 0.5]],
}


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")


def run_series(mesh, dt, series):
    previous = initial_state(JOB, mesh, series)
    previous_average = 293.15
    time = 0
    exact_delta = 0
    backward_delta = 0
    maximum_error = 0
    maximum_discrete_error = 0
    history = []
    capacity = 3.45e6 * (0.010 * 0.001 * 0.100)
    tau = 3.45e6 * 0.001 / (2 * 1000)
    step_number = 0
    while time < 6 - 1e-10:
        step = min(dt, 6 - time, math.floor(time + 1e-9) + 1 - time)
        phase = int(math.floor(time + 1e-9)) % 2
        q = 1e8 if phase == 0 else 0
        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))
        average = result["group_average_K"]["1"]
        steady = q * 0.001 / (2 * 1000)
        exact_delta = steady + (exact_delta - steady) * math.exp(-step / tau)
        backward_delta = (backward_delta + step * steady / tau) / (1 + step / tau)
        maximum_error = max(maximum_error, abs(average - 293.15 - exact_delta))
        maximum_discrete_error = max(
            maximum_discrete_error, abs(average - 293.15 - backward_delta)
        )
        storage = capacity * (average - previous_average) / step
        source = q * (0.010 * 0.001 * 0.100)
        residual = abs(storage + result["convection_W"] - source) / max(
            abs(storage), abs(source), abs(result["convection_W"]), 1e-20
        )
        check(
            "series-%d-step-%03d: накопление+теплоотдача" % (series, step_number),
            residual,
            1e-5,
        )
        time += step
        history.append(
            {
                "time_s": time,
                "T_average_K": average,
                "T_center_K": result["samples"][0]["T_K"],
                "analytic_lumped_K": 293.15 + exact_delta,
                "q_W_m3": q,
                "energy_balance_relative": residual,
            }
        )
        penultimate = previous
        previous = result["model"].replace(".feh", ".anh")
        previous_average = average
    check(
        "Неявная сосредоточенная контрольная схема, серия %d" % series,
        maximum_discrete_error,
        0.05,
    )
    return {
        "mesh_mm": mesh,
        "dt_s": dt,
        "model": result["model"],
        "previous_solution": penultimate,
        "maximum_continuous_error_K": maximum_error,
        "final_T_K": average,
        "history": history,
    }


for series, (mesh, dt) in enumerate(((0.2, 0.2), (0.1, 0.1), (0.05, 0.05)), 1):
    records.append(run_series(mesh, dt, series))
half = run_series(0.05, 0.025, 4)
records[-1]["half_dt"] = half
check(
    "Уточнение только времени при неизменной сетке",
    half["maximum_continuous_error_K"] / records[-1]["maximum_continuous_error_K"],
    0.65,
)
check(
    "Окончательная погрешность относительно непрерывной модели, К",
    half["maximum_continuous_error_K"],
    0.3,
)

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"]
