from __future__ import annotations
from dataclasses import dataclass
import matplotlib.pyplot as plt
import numpy as np
# ---------------------------------------------------------------------------
# Исходные данные — правите только этот блок
# ---------------------------------------------------------------------------
LAYERS = [
# имя, толщина мм, λ Вт/(м·К), μ
("Штукатурка внутр.", 15.0, 0.81, 10.0),
("Кирпич", 510.0, 0.56, 8.0),
("Минвата", 100.0, 0.040, 1.0),
("Штукатурка нар.", 20.0, 0.90, 15.0),
]
T_IN = 20.0 # °C, внутри
PHI_IN = 55.0 # %, внутри
T_OUT = -25.0 # °C, снаружи
PHI_OUT = 80.0 # %, снаружи
# Стена, горизонтальный поток. СП 50: 0.115 / 0.043; EN ISO 6946: 0.13 / 0.04
RSI = 0.115
RSE = 0.043
OUT_PNG = "wall_glaser.png"
# ---------------------------------------------------------------------------
# Физика
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class Layer:
name: str
thickness_mm: float
lambda_w: float
mu: float
def __post_init__(self) -> None:
if self.thickness_mm <= 0 or self.lambda_w <= 0 or self.mu <= 0:
raise ValueError(f"Некорректный слой: {self.name}")
@property
def d(self) -> float:
return self.thickness_mm / 1000.0
@property
def R(self) -> float:
return self.d / self.lambda_w
@property
def sd(self) -> float:
return self.mu * self.d
def saturation_pressure(theta_c: np.ndarray | float) -> np.ndarray | float:
"""pнас, Па. ISO 13788 (формула Магнуса)."""
scalar = np.isscalar(theta_c)
theta = np.atleast_1d(np.asarray(theta_c, dtype=float))
ps = np.empty_like(theta)
above = theta >= 0.0
ps[above] = 610.5 * np.exp(17.269 * theta[above] / (237.3 + theta[above]))
ps[~above] = 610.5 * np.exp(21.875 * theta[~above] / (265.5 + theta[~above]))
return float(ps) if scalar else ps
def dew_point(p_pa: float) -> float:
"""Обратный Магнус, °C. Для p от льда/воды — ветка по 610.5 Па."""
ln = np.log(p_pa / 610.5)
if p_pa >= 610.5:
return 237.3 * ln / (17.269 - ln)
return 265.5 * ln / (21.875 - ln)
def glaser_pressure(
sd: np.ndarray,
psat: np.ndarray,
p_in: float,
p_out: float,
) -> np.ndarray:
"""Паровое давление по Глазеру: отрезок pi–pe, при необходимости касание pнас."""
n = len(sd)
p = np.empty(n)
def fill(i: int, j: int, pi: float, pj: float) -> None:
if j <= i + 1:
p[i] = pi
p[j] = pj
return
s = sd[i : j + 1]
plin = pi + (pj - pi) * (s - sd[i]) / (sd[j] - sd[i])
over = plin - psat[i : j + 1]
k_rel = int(np.argmax(over))
if over[k_rel] <= 1.0: # 1 Па — численный допуск
p[i : j + 1] = plin
return
k = i + k_rel
if k == i or k == j:
p[i : j + 1] = np.minimum(plin, psat[i : j + 1])
return
fill(i, k, pi, float(psat[k]))
fill(k, j, float(psat[k]), pj)
fill(0, n - 1, p_in, p_out)
return p
def calculate(
layers: list[Layer],
t_in: float,
phi_in: float,
t_out: float,
phi_out: float,
rsi: float,
rse: float,
) -> dict:
r_layers = np.array([ly.R for ly in layers], dtype=float)
sd_layers = np.array([ly.sd for ly in layers], dtype=float)
d_layers = np.array([ly.d for ly in layers], dtype=float)
r_total = rsi + float(r_layers.sum()) + rse
u = 1.0 / r_total
q = (t_in - t_out) / r_total # Вт/м²
# Границы слоёв: 0 = внутренняя поверхность, n = наружная
r_cum = np.concatenate(([0.0], np.cumsum(r_layers)))
theta = t_in - q * (rsi + r_cum) # °C на границах
x_m = np.concatenate(([0.0], np.cumsum(d_layers)))
sd = np.concatenate(([0.0], np.cumsum(sd_layers)))
p_in = (phi_in / 100.0) * float(saturation_pressure(t_in))
p_out = (phi_out / 100.0) * float(saturation_pressure(t_out))
psat = np.asarray(saturation_pressure(theta), dtype=float)
p_free = p_in + (p_out - p_in) * sd / sd[-1]
p_glaser = glaser_pressure(sd, psat, p_in, p_out)
cond_iface = (p_glaser > psat + 1.0) | (
np.abs(p_glaser - psat) <= 1.0
) & (np.abs(p_free - psat) > 1.0)
# конденсат в толще: касание pнас на внутренней границе слоя, не на поверхностях «просто так»
cond_iface = False
cond_iface[-1] = p_glaser[-1] > psat[-1] + 1.0
has_interstitial = bool(np.any(p_free > psat + 1.0))
t_dp_in = dew_point(p_in)
surface_cond = theta < t_dp_in - 0.05
return {
"layers": layers,
"r_layers": r_layers,
"r_total": r_total,
"u": u,
"q": q,
"theta": theta,
"x_mm": x_m * 1000.0,
"sd": sd,
"psat": psat,
"p_free": p_free,
"p_glaser": p_glaser,
"p_in": p_in,
"p_out": p_out,
"t_dp_in": t_dp_in,
"has_interstitial": has_interstitial,
"surface_cond": surface_cond,
"t_in": t_in,
"t_out": t_out,
"phi_in": phi_in,
"phi_out": phi_out,
"rsi": rsi,
"rse": rse,
}
def print_report(res: dict) -> None:
layers: list[Layer] = res["layers"]
theta = res["theta"]
print("=" * 72)
print(f"Rsi={res['rsi']:.3f} Rse={res['rse']:.3f} "
f"R={res['r_total']:.3f} м²·К/Вт U={res['u']:.3f} Вт/(м²·К)")
print(f"q={res['q']:.1f} Вт/м²")
print(f"tв={res['t_in']:.1f} °C, φв={res['phi_in']:.0f}% → "
f"pв={res['p_in']:.0f} Па, tр={res['t_dp_in']:.1f} °C")
print(f"tн={res['t_out']:.1f} °C, φн={res['phi_out']:.0f}% → "
f"pн={res['p_out']:.0f} Па")
print(f"θвн.пов={theta:.2f} °C "
f"поверхностный конденсат: {'ДА' if res['surface_cond'] else 'нет'}")
print(f"конденсат в толще (Глазер): "
f"{'ДА' if res['has_interstitial'] else 'нет'}")
print("-" * 72)
print(f"{'#':<3} {'слой':<22} {'d,мм':>6} {'λ':>6} {'μ':>5} "
f"{'R':>7} {'sd,м':>7} {'θнар,°C':>8}")
for i, ly in enumerate(layers):
print(f"{i+1:<3} {ly.name:<22} {ly.thickness_mm:6.1f} {ly.lambda_w:6.3f} "
f"{ly.mu:5.1f} {ly.R:7.3f} {ly.sd:7.3f} {theta[i+1]:8.2f}")
print("=" * 72)
def plot_results(res: dict, path: str) -> None:
plt.rcParams["font.family"] = "DejaVu Sans"
plt.rcParams["axes.grid"] = True
plt.rcParams["grid.alpha"] = 0.35
layers: list[Layer] = res["layers"]
x = res["x_mm"]
theta = res["theta"]
sd = res["sd"]
colors = plt.cm.tab10(np.linspace(0, 0.9, len(layers)))
fig, (ax_t, ax_p) = plt.subplots(2, 1, figsize=(11, 8), layout="constrained")
# --- температура по толще ---
for i, ly in enumerate(layers):
ax_t.axvspan(x[i], x[i + 1], color=colors[i], alpha=0.25, zorder=0)
ax_t.text(
0.5 * (x[i] + x[i + 1]),
ax_t.get_ylim() if False else theta.min(),
ly.name,
ha="center",
va="bottom",
fontsize=8,
rotation=90 if ly.thickness_mm < 40 else 0,
)
ax_t.plot(x, theta, color="C3", lw=2.2, marker="o", label="θ, °C")
ax_t.axhline(res["t_in"], color="C1", ls="--", lw=1, label=f"tв = {res['t_in']:.1f} °C")
ax_t.axhline(res["t_out"], color="C0", ls="--", lw=1, label=f"tн = {res['t_out']:.1f} °C")
ax_t.axhline(
res["t_dp_in"],
color="C4",
ls=":",
lw=1.4,
label=f"точка росы воздуха tр = {res['t_dp_in']:.1f} °C",
)
ax_t.set_xlim(x, x[-1])
ax_t.set_xlabel("толщина от внутренней поверхности, мм")
ax_t.set_ylabel("температура, °C")
ax_t.set_title("Профиль температуры по слоям")
ax_t.legend(loc="best", fontsize=8)
# подписи слоёв после автомасштаба
y_txt = ax_t.get_ylim() + 0.04 * (ax_t.get_ylim() - ax_t.get_ylim())
for i, ly in enumerate(layers):
ax_t.text(
0.5 * (x[i] + x[i + 1]),
y_txt,
ly.name,
ha="center",
va="bottom",
fontsize=8,
color="0.2",
)
# --- Глазер: p и pнас по sd ---
ax_p.plot(sd, res["psat"] / 1000.0, color="C0", lw=2.2, marker="s",
label="pнас (по θ границ)")
ax_p.plot(sd, res["p_free"] / 1000.0, color="0.4", lw=1.4, ls="--",
label="p без конденсата (прямая pi–pe)")
ax_p.plot(sd, res["p_glaser"] / 1000.0, color="C3", lw=2.0, marker="o",
label="p по Глазеру")
ax_p.fill_between(
sd,
res["p_glaser"] / 1000.0,
res["psat"] / 1000.0,
where=res["p_free"] > res["psat"],
color="C3",
alpha=0.25,
interpolate=True,
label="зона конденсата",
)
for i, ly in enumerate(layers):
ax_p.axvline(sd[i], color="0.8", lw=0.8)
ax_p.text(
0.5 * (sd[i] + sd[i + 1]),
ax_p.get_ylim() if False else 0,
ly.name,
ha="center",
fontsize=8,
)
ax_p.set_xlim(sd, sd[-1])
ax_p.set_xlabel("sd = μ·d, м")
ax_p.set_ylabel("давление пара, кПа")
title = "Глазер / ISO 13788"
if res["has_interstitial"]:
title += " — конденсат в толще"
elif res["surface_cond"]:
title += " — конденсат на внутренней поверхности"
else:
title += " — конденсата нет"
ax_p.set_title(title)
ax_p.legend(loc="best", fontsize=8)
y_txt_p = ax_p.get_ylim() + 0.04 * (ax_p.get_ylim() - ax_p.get_ylim())
for i, ly in enumerate(layers):
ax_p.text(
0.5 * (sd[i] + sd[i + 1]),
y_txt_p,
ly.name,
ha="center",
va="bottom",
fontsize=8,
color="0.2",
)
fig.savefig(path, dpi=150)
print(f"график: {path}")
plt.show()
if __name__ == "__main__":
layers = [Layer(*row) for row in LAYERS]
res = calculate(layers, T_IN, PHI_IN, T_OUT, PHI_OUT, RSI, RSE)
print_report(res)
plot_results(res, OUT_PNG)