FEMM 5

Электрическое поле тока · Осесимметричная

Кондуктометрический датчик уровня жидкости

Коаксиальные электроды радиусами 2 и 10 мм, высота чувствительной части 100 мм. Жидкость занимает 20, 50 или 80 мм снизу. Расчёт осесимметричный: горизонтальная координата r, вертикальная z.

Работа связывает полевое распределение с измеряемой электротехнической величиной. Все исходные данные заданы в программе; принятые идеализации сформулированы до расчёта. Основной результат проверяется независимой формулой или балансом, а чувствительность к геометрии исследуется отдельными вариантами.

Самостоятельная учебная модель FEMM 5: от физических соотношений к полевому решению и проверке результата.

Допущения и устройство учебной модели

Все размеры, свойства материалов и условия возбуждения заданы ниже. Численные результаты относятся к опубликованной версии модели FEMM 5.

Физическая и математическая постановка

Теория и физическая постановка

Каждый горизонтальный слой образует самостоятельный радиальный путь проводимости между цилиндрическими электродами. Слои по высоте включены параллельно. Поэтому полная проводимость линейно зависит от заполненной высоты при неизменной проводимости жидкости и неизменном сечении. Радиальная площадь меняется вдоль пути тока, что приводит к логарифму отношения радиусов.

Жидкость и сухая область разделены горизонтальной границей. Обе цилиндрические стенки имеют постоянные потенциалы по всей высоте, торцы электрически изолированы. При такой постановке аналитический потенциал не зависит от z, а нормальный ток через границу уровня равен нулю. FEMM должен получить это распределение самостоятельно, используя разные проводимости материалов.

Сухой среде назначена малая положительная проводимость 10⁻⁹ См/м. Это явный паразитный путь, а не математический вакуум. В контрольной формуле он учитывается, хотя его влияние мало. Выбор осесимметрии принципиален: плоская прямоугольная модель давала бы другую площадь тока и другой закон зависимости от радиусов.

G(h)=\frac{2\pi[\sigma_lh+\sigma_d(H-h)]}{\ln(b/a)}

Как читать расчёт

Программа строит три сетки, сохраняет каждую модель, закрывает её и повторно открывает перед решением. Поэтому проверка относится к точному записанному файлу с русскими именами материалов, проводников и граничных условий. Результаты каждой серии находятся в новой папке calculated-XX рядом с программой; предыдущая серия не перезаписывается.

В result.json массив records содержит расчётные величины и параметры сетки, checks — фактические отклонения, допустимые пределы и логический результат. Проверяйте все критерии одновременно. Совпадение одной точки с ориентиром не заменяет проверки баланса и изменения результата по сетке. Для итерационной обратной связи отдельно контролируется сходимость итераций; она не является оценкой пространственной погрешности.

Практическое исследование

Измените проводимость жидкости в два раза и сравните изменение тока с изменением уровня. Объясните, почему без дополнительного измерения температуры или состава эти два воздействия трудно разделить по одному электрическому сигналу.

Измените отношение радиусов, сохранив наружный размер или ширину зазора. Сопоставьте логарифмическую формулу с ошибочной формулой плоской щели и определите, при каком малом зазоре последнее приближение становится приемлемым.

Увеличьте проводимость сухой области. Найдите ненулевой отсчёт при пустом датчике и объясните необходимость его калибровки. Сравните абсолютную проводимость и приращение относительно пустого состояния.

Исходные данные

Коаксиальные электроды радиусами 2 и 10 мм, высота чувствительной части 100 мм. Жидкость занимает 20, 50 или 80 мм снизу. Расчёт осесимметричный: горизонтальная координата r, вертикальная z.

Величины, единицы и происхождение параметров
ПараметрЗначениеЕдиницаПроисхождение
Внутренний радиус2ммПринято в учебной постановке
Наружный радиус10ммПринято в учебной постановке
Высота100ммПринято в учебной постановке
σ жидкости0,02См/мПринято в учебной постановке
Напряжение1ВПринято в учебной постановке

Порядок воспроизведения

  1. Скопируйте reproduce.py в отдельную рабочую папку и откройте встроенным редактором Python FEMM 5. Проверьте заданные параметры и единицы, затем выполните программу целиком. Внешний модуль pyFEMM для этой программы не используется.
  2. Откройте модель и решение окончательной сетки. Сопоставьте материалы, проводники и границы с теорией. Для осесимметричной задачи найдите ось вращения и объясните, какие поверхности образуются при вращении каждой линии поперечного рисунка.
  3. Составьте таблицу трёх сеток и приведите независимый расчёт по формуле. Для серии из нескольких геометрий сравнивайте только результаты одной и той же сетки, затем отдельно исследуйте её уточнение. Укажите абсолютные и относительные ошибки с единицами.
  4. Выполните предложенные варианты по одному. До запуска предскажите знак и примерный масштаб эффекта. В отчёте объясните, какие величины действительно определяет модель, какие параметры остаются предположениями и что пришлось бы уточнить для реального изделия.

Скачайте нужные файлы по отдельным ссылкам ниже в одну рабочую папку, затем откройте reproduce.py во встроенном Python FEMM 5 и выполните его. Сохраните имена файлов и структуру подпапок. Условия конкретного запуска и путь новых результатов указаны в комментариях программы.

Программа встроенного Python FEMM 5
Python · reproduce.py
"""Самостоятельная лабораторная работа курса 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", []) + job.get("segments", []):
        globals()[prefix + "_addnode"](x1, y1)
        globals()[prefix + "_addnode"](x2, y2)
        globals()[prefix + "_addsegment"](x1, y1, x2, y2)
    for x1, y1, x2, y2, angle, name in job.get("arcs", []):
        globals()[prefix + "_addnode"](x1, y1)
        globals()[prefix + "_addnode"](x2, y2)
        globals()[prefix + "_addarc"](x1, y1, x2, y2, angle, 1)
        cx, cy = job.get("arc_center", (0, 0))
        mx = (x1 + x2) / 2 - cx
        my = (y1 + y2) / 2 - cy
        radius = math.sqrt((x1 - cx) ** 2 + (y1 - cy) ** 2)
        factor = radius / math.sqrt(mx * mx + my * my)
        globals()[prefix + "_selectarcsegment"](cx + mx * factor, cy + my * factor)
        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, 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
            * job.get("block_mesh_multipliers", {}).get(
                str(group), job.get("mesh_multipliers", {}).get(name, 1)
            ),
            group,
        )
        globals()[prefix + "_clearselected"]()


def current(job, mesh, stem):
    newdocument(3)
    ci_probdef("millimeters", job.get("type", "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", job.get("type", "planar"), 1e-10, job["depth"], 30)
    for name, kx, ky, q in job["materials"]:
        hi_addmaterial(name, kx, ky, q, 0)
    for boundary in job["boundaries"]:
        name, kind, temp, ambient, h = boundary[:5]
        beta = boundary[5] if len(boundary) > 5 else 0
        hi_addboundprop(name, kind, temp, 0, ambient, h, beta)
    geometry("hi", job, mesh)
    for x, y, name in job.get("boundary_segments", []):
        hi_selectsegment(x, y)
        hi_setsegmentprop(name, job.get("boundary_mesh", 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 = {
    "type": "axi",
    "polygon": [[2, 0], [10, 0], [10, 100], [2, 100]],
    "depth": 1,
    "materials": [["Проводящая жидкость", 0.02, 0.02], ["Сухая среда", 1e-09, 1e-09]],
    "terminals": [["Внутренний электрод", 1, 0, 1], ["Наружный электрод", 0, 0, 1]],
    "blocks": [],
}
for level, mesh in enumerate((0.5, 0.25, 0.125)):
    readings = []
    for height in (20, 50, 80):
        job = json.loads(json.dumps(JOB))
        job["interfaces"] = [[2, height, 10, height]]
        job["blocks"] = [
            [6, height / 2, "Проводящая жидкость", 1],
            [6, (height + 100) / 2, "Сухая среда", 2],
        ]
        job["electrode_segments"] = [
            [2, height / 2, "Внутренний электрод"],
            [2, (height + 100) / 2, "Внутренний электрод"],
            [10, height / 2, "Наружный электрод"],
            [10, (height + 100) / 2, "Наружный электрод"],
        ]
        row = current(job, mesh, "mesh-%d-level-%d" % (level + 1, height))
        conductance = abs(row["terminals"]["Внутренний электрод"]["I_A"])
        reference = (
            2
            * math.pi
            * (0.02 * height * 0.001 + 1e-9 * (100 - height) * 0.001)
            / math.log(10 / 2)
        )
        check(
            "Проводимость уровня %d мм, сетка %d" % (height, level + 1),
            abs(conductance / reference - 1),
            0.002,
        )
        row["level_mm"] = height
        row["conductance_S"] = conductance
        readings.append(row)
    records.append(
        {"mesh_mm": mesh, "readings": readings, "model": readings[-1]["model"]}
    )
check(
    "Сходимость датчика",
    abs(
        records[-1]["readings"][1]["conductance_S"]
        / records[-2]["readings"][1]["conductance_S"]
        - 1
    ),
    0.002,
)

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"]

Картина рассчитанного поля

Потенциал, В: mesh-3-level-80.anc
Визуализация сохранённого решения FEMM 5 mesh-3-level-80.anc. Электрический потенциал по сохранённым узловым значениям. Показана расчётная область. Контуры деталей взяты из той же модели. Области вне сохранённых треугольников не заполняются. Это научный график сохранённых данных. Нажмите на карту, чтобы открыть её отдельно и увеличить.

Окна FEMM 5

Снимки интерфейса FEMM показывают геометрию, настройки и работу с результатами в FEMM 5. Откройте изображение, чтобы рассмотреть подписи и значения в полном размере.

Рабочая область модели «Кондуктометрический датчик уровня жидкости». Нативный препроцессор FEMM 5.
Рабочая область модели «Кондуктометрический датчик уровня жидкости». Нативный препроцессор FEMM 5. Открыть в полном размере
Сохранённое решение «Кондуктометрический датчик уровня жидкости». Модуль потенциала |V|, В, в осесимметричном резистивном уровнемере; стационарное питание 1 В. Эквипотенциали скрыты для читаемости узкого разреза.
Сохранённое решение «Кондуктометрический датчик уровня жидкости». Модуль потенциала |V|, В, в осесимметричном резистивном уровнемере; стационарное питание 1 В. Эквипотенциали скрыты для читаемости узкого разреза. Открыть в полном размере

Результаты и численные проверки

Сохранено решений FEMM: 9. Выполнено проверок: 28. Полная точность чисел и условия сравнения доступны в таблицах результатов JSON.

Результаты расчётов и производные итоги
Масштаб сетки, ммМодельУровень жидкости, ммМощность области, ВтПроводимость, СмЭлементовКонтрольные точкиЭлектродыОтносительная невязка токаОтносительная невязка мощностиМощность, ВтУзлов
0,5mesh-1-level-20.fec202 полей; полные данные JSON0,001563521880120 значений; полные данные JSON2 полей; полные данные JSON15,638922 × 10⁻¹²32,755556 × 10⁻¹²0,00156352184319
0,5mesh-1-level-50.fec502 полей; полные данные JSON0,003908897282650 значений; полные данные JSON2 полей; полные данные JSON244,02618 × 10⁻¹²44,700593 × 10⁻¹²0,00390889724415
0,5mesh-1-level-80.fec802 полей; полные данные JSON0,00625454478820 значений; полные данные JSON2 полей; полные данные JSON49,239923 × 10⁻¹²4,2751362 × 10⁻¹²0,0062545444273
0,25mesh-2-level-20.fec202 полей; полные данные JSON0,001562126307470 значений; полные данные JSON2 полей; полные данные JSON36,162374 × 10⁻¹²35,895302 × 10⁻¹²0,00156212616016
0,25mesh-2-level-50.fec502 полей; полные данные JSON0,0039052716312430 значений; полные данные JSON2 полей; полные данные JSON234,54628 × 10⁻¹²4,9579435 × 10⁻¹²0,003905271616175
0,25mesh-2-level-80.fec802 полей; полные данные JSON0,0062487108307980 значений; полные данные JSON2 полей; полные данные JSON943,52171 × 10⁻¹²37,214858 × 10⁻¹²0,006248710816030
0,125mesh-3-level-20.fec202 полей; полные данные JSON0,00156173051214910 значений; полные данные JSON2 полей; полные данные JSON730,62402 × 10⁻¹²254,6775 × 10⁻¹²0,001561730561984
0,125mesh-3-level-50.fec502 полей; полные данные JSON0,00390432131216860 значений; полные данные JSON2 полей; полные данные JSON290,52039 × 10⁻¹²194,87919 × 10⁻¹²0,003904321361949
0,125mesh-3-level-80.fec802 полей; полные данные JSON0,00624692491211120 значений; полные данные JSON2 полей; полные данные JSON334,13322 × 10⁻¹²195,48933 × 10⁻¹²0,006246924961837
Итоги исходных серий
Сводные записи; отдельные показания приведены выше
Контрольные показанияМасштаб сетки, ммМодель
3 точек; полные данные JSON0,5mesh-1-level-80.fec
3 точек; полные данные JSON0,25mesh-2-level-80.fec
3 точек; полные данные JSON0,125mesh-3-level-80.fec
Проверки опубликованного расчёта
КритерийПолученоДопуск или ожидаемое значениеИтог
mesh-1-level-20: баланс токов15,64 × 10⁻¹²1e-06Выполнен
mesh-1-level-20: баланс мощности32,76 × 10⁻¹²1e-06Выполнен
Проводимость уровня 20 мм, сетка 10,001240,002Выполнен
mesh-1-level-50: баланс токов244 × 10⁻¹²1e-06Выполнен
mesh-1-level-50: баланс мощности44,7 × 10⁻¹²1e-06Выполнен
Проводимость уровня 50 мм, сетка 10,0012640,002Выполнен
mesh-1-level-80: баланс токов49,24 × 10⁻¹²1e-06Выполнен
mesh-1-level-80: баланс мощности4,275 × 10⁻¹²1e-06Выполнен
Проводимость уровня 80 мм, сетка 10,0013130,002Выполнен
mesh-2-level-20: баланс токов36,16 × 10⁻¹²1e-06Выполнен
mesh-2-level-20: баланс мощности35,9 × 10⁻¹²1e-06Выполнен
Проводимость уровня 20 мм, сетка 2346,2 × 10⁻⁶0,002Выполнен
mesh-2-level-50: баланс токов234,5 × 10⁻¹²1e-06Выполнен
mesh-2-level-50: баланс мощности4,958 × 10⁻¹²1e-06Выполнен
Проводимость уровня 50 мм, сетка 2335,3 × 10⁻⁶0,002Выполнен
mesh-2-level-80: баланс токов943,5 × 10⁻¹²1e-06Выполнен
mesh-2-level-80: баланс мощности37,21 × 10⁻¹²1e-06Выполнен
Проводимость уровня 80 мм, сетка 2379,5 × 10⁻⁶0,002Выполнен
mesh-3-level-20: баланс токов730,6 × 10⁻¹²1e-06Выполнен
mesh-3-level-20: баланс мощности254,7 × 10⁻¹²1e-06Выполнен
Проводимость уровня 20 мм, сетка 392,94 × 10⁻⁶0,002Выполнен
mesh-3-level-50: баланс токов290,5 × 10⁻¹²1e-06Выполнен
mesh-3-level-50: баланс мощности194,9 × 10⁻¹²1e-06Выполнен
Проводимость уровня 50 мм, сетка 391,85 × 10⁻⁶0,002Выполнен
mesh-3-level-80: баланс токов334,1 × 10⁻¹²1e-06Выполнен
mesh-3-level-80: баланс мощности195,5 × 10⁻¹²1e-06Выполнен
Проводимость уровня 80 мм, сетка 393,62 × 10⁻⁶0,002Выполнен
Сходимость датчика243,3 × 10⁻⁶0,002Выполнен

Допуски относятся к указанной учебной задаче и способу сравнения. Число показанных цифр не означает такой же физической точности.

Модели, решения и программа

Для нового расчёта сохраните программу, модели и необходимые данные в одну рабочую папку, сохраняя структуру подпапок.

Полные численные результаты

Границы применимости