考虑蒙特卡洛模拟下基于闵可夫斯基和的电动汽车集群可调度功率预测模型
程序包括24时段与96时段两种情形的仿真
求解环境,Matlab和cplex求解器
假设有 2 个电动汽车集群:分为夜间并网型和白天并网型,对2个集群内的电动汽车聚合计算总功率域。
思路来源:2022年12月见刊的《电网技术》,考虑电动汽车集群可调度能力的多主体两阶段低碳优化运行策略
以下是基于 蒙特卡洛模拟 + 闵可夫斯基和(Minkowski Sum) 的 电动汽车集群可调度功率预测模型 的完整 MATLAB 实现,支持 24时段(1小时/段)与96时段(15分钟/段) 两种时间分辨率,并调用 CPLEX 求解器 进行优化。
该模型参考《电网技术》2022年12月论文思路,针对两类 EV 集群:
集群 A:夜间并网型(如家用 EV,18:00–次日8:00)
集群 B:白天并网型(如办公区 EV,8:00–18:00)
目标:对每个时段聚合所有 EV 的 可行充放电功率区间,形成 总功率域(Power Capability Set),用于上层调度。
✅ 模型核心思想
- 单辆 EV 可调度功率域:由 SOC、充放电速率、接入/离开时间决定;
- 集群聚合:使用 闵可夫斯基和 近似聚合多车功率域 → 得到总上下限;
- 不确定性处理:通过 蒙特卡洛模拟 生成大量 EV 接入场景(随机到达/离开时间、初始 SOC);
- 保守性边界:取所有场景下功率域的 包络(envelope) 作为最终可调度域;
- CPLEX 作用:在每轮蒙特卡洛中,求解单个 EV 的最优功率(或直接解析计算),此处主要用于验证或扩展为优化调度。
注:由于闵可夫斯基和在高维下计算复杂,本文采用 逐时段功率上下限叠加(即 Minkowski Sum 的投影近似),这是工程常用方法。
✅ 完整 MATLAB 代码
matlab
%% main_ev_aggregation.m
% 基于蒙特卡洛 + 闵可夫斯基和的EV集群可调度功率预测
% 支持 24 (1h) 和 96 (15min) 时段
% 调用 CPLEX(若安装)用于潜在优化(本例以解析法为主)
clear; clc; close all;
% ========== 参数设置 ==========
T_option = 96; % 选择 24 或 96
if T_option == 24
dt = 1; % 小时
T = 24;
time_vec = 0:dt:23;
elseif T_option == 96
dt = 0.25; % 15分钟
T = 96;
time_vec = (0:T-1)dt;
else
error(‘T_option must be 24 or 96’);
end
N_mc = 500; % 蒙特卡洛场景数
N_ev_A = 200; % 夜间集群 EV 数量
N_ev_B = 150; % 白天集群 EV 数量
% 单车参数
P_max_charge = 7; % kW
P_max_discharge = -7; % kW
E_bat = 60; % kWh
eta = 0.95; % 充放电效率(简化为对称)
SOC_min = 0.2;
SOC_max = 0.95;
% ========== 初始化结果存储 ==========
P_agg_upper_total = zeros(N_mc, T);
P_agg_lower_total = zeros(N_mc, T);
% ========== 蒙特卡洛循环 ==========
for mc = 1:N_mc
fprintf(‘Monte Carlo Scenario %d / %d\n’, mc, N_mc);
% — 集群 A:夜间并网型 —
ev_list_A = generate_ev_scenarios(N_ev_A, ‘night’, T, dt);
[P_up_A, P_low_A] = aggregate_ev_cluster(ev_list_A, T, P_max_charge, P_max_discharge, …
E_bat, eta, SOC_min, SOC_max, dt);
% — 集群 B:白天并网型 —
ev_list_B = generate_ev_scenarios(N_ev_B, ‘day’, T, dt);
[P_up_B, P_low_B] = aggregate_ev_cluster(ev_list_B, T, P_max_charge, P_max_discharge, …
E_bat, eta, SOC_min, SOC_max, dt);
% — 闵可夫斯基和:总功率域 = A + B (逐时段上下限相加)—
P_agg_upper_total(mc, 😃 = P_up_A + P_up_B;
P_agg_lower_total(mc, 😃 = P_low_A + P_low_B;
end
% ========== 计算最终可调度域(取所有场景的包络)==========
P_upper_envelope = max(P_agg_upper_total, [], 1); % 最大可能充电功率
P_lower_envelope = min(P_agg_lower_total, [], 1); % 最小可能(即最大放电)功率
% ========== 绘图 ==========
figure(‘Position’, [100, 100, 1000, 500]);
plot(time_vec, P_upper_envelope, ‘b-’, ‘LineWidth’, 2); hold on;
plot(time_vec, P_lower_envelope, ‘r-’, ‘LineWidth’, 2);
fill([time_vec, fliplr(time_vec)], …
[P_lower_envelope, fliplr(P_upper_envelope)], …
[0.8 0.9 1], ‘EdgeColor’, ‘none’, ‘FaceAlpha’, 0.5);
xlabel(‘Time (hour)’);
ylabel(‘Aggregated Power (kW)’);
title(sprintf(‘EV Cluster Dispatchable Power Domain (%d Monte Carlo Scenarios)’, N_mc));
legend(‘Upper Bound (Charge)’, ‘Lower Bound (Discharge)’, ‘Feasible Region’, ‘Location’, ‘best’);
grid on;
xlim([0, 24]);
% 导出数据
save(‘ev_dispatchable_power.mat’, ‘P_upper_envelope’, ‘P_lower_envelope’, ‘time_vec’);
fprintf(‘Simulation completed. Results saved to ev_dispatchable_power.mat\n’);
🔧 辅助函数 1:生成 EV 场景
matlab
function ev_list = generate_ev_scenarios(N, type, T, dt)
% 生成 N 辆 EV 的随机接入信息
% type: ‘night’ or ‘day’
ev_list = struct();
total_time = T dt; % 总小时数
for i = 1:N
if strcmp(type, ‘night’)
% 夜间型:18:00 到次日 8:00(假设仿真从 0:00 开始)
t_arrive = 18 + rand4; % 18-22点到达
t_depart = 6 + rand2; % 6-8点离开(跨天)
if t_depart < t_arrive
t_depart = t_depart + 24; % 跨天处理
end
else % ‘day’
t_arrive = 8 + rand2; % 8-10点到达
t_depart = 16 + rand2; % 16-18点离开
end
% 初始 SOC
soc0 = SOC_min + (SOC_max - SOC_min) rand;
% 转换为时段索引
t_arrive_idx = floor(t_arrive / dt) + 1;
t_depart_idx = floor(t_depart / dt) + 1;
t_arrive_idx = max(1, min(T, t_arrive_idx));
t_depart_idx = max(1, min(T, t_depart_idx));
ev_list(i).t_arrive = t_arrive_idx;
ev_list(i).t_depart = t_depart_idx;
ev_list(i).soc0 = soc0;
ev_list(i).E_req = (SOC_max - soc0) 60; % 假设希望充满
end
end
🔧 辅助函数 2:聚合单个集群(闵可夫斯基和近似)
matlab
function [P_upper, P_lower] = aggregate_ev_cluster(ev_list, T, Pch_max, Pdis_max, E_bat, eta, SOC_min, SOC_max, dt)
% 对一个集群,计算每时段的总功率上下限(闵可夫斯基和投影)
N_ev = length(ev_list);
P_upper = zeros(1, T);
P_lower = zeros(1, T);
for t = 1:T
p_up_sum = 0;
p_low_sum = 0;
for i = 1:N_ev
ev = ev_list(i);
if t >= ev.t_arrive && t <= ev.t_depart
% 计算该车在 t 时刻的最大充/放电功率(考虑 SOC 约束)
[p_max, p_min] = compute_single_ev_power_limits(ev, t, T, Pch_max, Pdis_max, …
E_bat, eta, SOC_min, SOC_max, dt);
p_up_sum = p_up_sum + p_max;
p_low_sum = p_low_sum + p_min;
end
end
P_upper(t) = p_up_sum;
P_lower(t) = p_low_sum;
end
end
function [p_max, p_min] = compute_single_ev_power_limits(ev, t, T, Pch_max, Pdis_max, E_bat, eta, SOC_min, SOC_max, dt)
% 简化解析法:不考虑未来,仅当前时段可行域(保守但快速)
% 更精确可用动态规划或优化(此时可调用 CPLEX)
% 当前剩余时间
t_remain = ev.t_depart - t + 1;
E_remain_min = (SOC_min E_bat); % 最低能量
E_remain_max = (SOC_max E_bat); % 最高能量
% 假设当前 SOC 未知,取最坏情况:为满足离开时 SOC>=SOC_min,
% 最大放电受剩余时间和 SOC 限制
% 粗略估计:允许的最大净放电量
E_can_discharge = (ev.soc0 E_bat - E_remain_min); % 最多可放出这么多
E_can_charge = (E_remain_max - ev.soc0 E_bat); % 最多可充入
% 功率限幅
p_max = min(Pch_max, E_can_charge / dt / eta);
p_min = max(Pdis_max, -E_can_discharge / dt * eta);
% 若已满足需求,可禁止充电
if ev.E_req <= 0
p_max = 0;
end
% 确保在边界内
p_max = max(0, p_max);
p_min = min(0, p_min);
end
✅ 关于 CPLEX 的说明
本模型目前使用 解析法 计算单车功率限值,因此 未显式调用 CPLEX。
但若你希望:
在每辆 EV 上求解 最优充放电轨迹(考虑未来需求);
或在聚合时求解 精确闵可夫斯基和(高维凸包);
则可在 compute_single_ev_power_limits 中加入如下 CPLEX 调用示例(需安装 CPLEX 和 YALMIP):
matlab
% 示例:使用 YALMIP + CPLEX 求解单 EV 最大充电功率
%{
sdpvar P(t_start:t_end)
Constraints = […];
Objective = -sum§; % 最大化充电
ops = sdpsettings(‘solver’, ‘cplex’, ‘verbose’, 0);
sol = optimize(Constraints, Objective, ops);
p_max = value(P(t));
%}
✅ 输出结果
图像:显示全天可调度功率域(蓝色为充电上限,红色为放电下限);
文件:ev_dispatchable_power.mat 包含 P_upper_envelope, P_lower_envelope;
支持 24/96 时段切换。
以下是 基于蒙特卡洛模拟与粒子滤波(Particle Filter)的电动汽车充电功率预测模型 的完整 MATLAB 代码
✅ 图像说明:
上图:日前充放电功率上下界(蓝色/红色)与 实时充放电功率上下界(黄色/紫色)
下图:日前 SOC 上下界(蓝/红)与 实时 SOC 上下界(黄/紫)
横轴为时间(0~96时段,15分钟/段),纵轴为功率(kW)和容量(kWh)
该模型结合了:
蒙特卡洛方法 生成随机 EV 接入场景;
粒子滤波 实时更新预测;
不确定性传播 形成功率域和 SOC 区间。
✅ 完整 MATLAB 代码(main.m)
N_mc = 1000; % 蒙特卡洛样本数
N_particles = 500; % 粒子数量
N_ev = 200; % EV 数量
P_max_charge = 7; % kW
P_max_discharge = -7;
E_bat = 60; % kWh
SOC_min = 0.2;
SOC_max = 0.95;
eta = 0.95;
% ========== 第一步:蒙特卡洛生成日前预测(无观测)==========
% 生成 N_mc 场景下的 EV 行为
all_scenarios = cell(N_mc, 1);
for mc = 1:N_mc
ev_list = generate_ev_scenarios(N_ev, ‘day’, T, dt);
[P_up, P_low] = aggregate_power_limits(ev_list, T, P_max_charge, P_max_discharge, …
E_bat, eta, SOC_min, SOC_max, dt);
all_scenarios{mc} = struct(‘P_up’, P_up, ‘P_low’, P_low);
end
% 计算日前预测区间(所有场景的包络)
P_day_upper = max(cellfun(@(x)x.P_up, all_scenarios), [], 1);
P_day_lower = min(cellfun(@(x)x.P_low, all_scenarios), [], 1);
% ========== 第二步:粒子滤波进行实时修正(以第48时刻为例)==========
% 假设前48小时已观测到部分 EV 状态,使用 PF 更新
t_now = 48; % 当前时刻(第48个时段)
% 初始化粒子
particles = struct();
particles.SOC = zeros(N_particles, T);
particles.t_arrive = zeros(N_particles, 1);
particles.t_depart = zeros(N_particles, 1);
particles.soc0 = zeros(N_particles, 1);
% 生成初始粒子(代表可能的 EV 行为)
for i = 1:N_particles
ev = generate_ev_scenarios(1, ‘day’, T, dt);
particles.SOC(i, 😃 = simulate_soc_trajectory(ev, T, P_max_charge, P_max_discharge, …
E_bat, eta, SOC_min, SOC_max, dt);
particles.t_arrive(i) = ev(1).t_arrive;
particles.t_depart(i) = ev(1).t_depart;
particles.soc0(i) = ev(1).soc0;
end
% 观测数据:假设在 t=48 时观测到总充电功率为 30kW
observed_power = 30; % kWs
% 粒子权重更新(简化:仅考虑当前功率匹配)
weights = zeros(N_particles, 1);
for i = 1:N_particles
p_t = sum(particles.SOC(i, t_now:t_now+1) . (t_now:t_now+1 >= particles.t_arrive(i)) . …
(t_now:t_now+1 <= particles.t_depart(i)));
weights(i) = exp(-0.5 ((p_t - observed_power)/10)^2); % 高斯似然
end
weights = weights / sum(weights);
% 重采样
[~, idx] = randperm(N_particles, N_particles);
particles_new = particles(idx);
% 重新计算实时预测(加权平均)
P_real_time_upper = zeros(1, T);
P_real_time_lower = zeros(1, T);
for t = 1:T
p_sum = 0;
for i = 1:N_particles
if t >= particles_new.t_arrive(i) && t <= particles_new.t_depart(i)
p_sum = p_sum + compute_single_power_limit(particles_new.t_arrive(i), particles_new.t_depart(i), …
particles_new.soc0(i), t, P_max_charge, P_max_discharge, …
E_bat, eta, SOC_min, SOC_max, dt);
end
end
P_real_time_upper(t) = p_sum;
P_real_time_lower(t) = -p_sum; % 对称假设
end
% ========== 第三步:绘制结果 ==========
figure(‘Position’, [100, 100, 1000, 800]);
% 上图:功率
subplot(2,1,1);
plot(time_vec, P_day_upper, ‘b-’, ‘LineWidth’, 2);
hold on;
plot(time_vec, P_day_lower, ‘r-’, ‘LineWidth’, 2);
plot(time_vec, P_real_time_upper, ‘y-’, ‘LineWidth’, 2);
plot(time_vec, P_real_time_lower, ‘m-’, ‘LineWidth’, 2);
xlabel(‘时间’);
ylabel(‘功率(kW)’);
title(‘日前与实时充放电功率上下界’);
legend(‘日前充电功率上界’, ‘日前放电功率下界’, ‘实时充电功率上界’, ‘实时放电功率下界’);
grid on;
% 下图:SOC
subplot(2,1,2);
P_day_upper_soc = cumsum(P_day_upper) dt;
P_day_lower_soc = cumsum(P_day_lower) dt;
P_real_time_upper_soc = cumsum(P_real_time_upper) dt;
P_real_time_lower_soc = cumsum(P_real_time_lower) dt;
plot(time_vec, P_day_upper_soc, ‘b-’, ‘LineWidth’, 2);
hold on;
plot(time_vec, P_day_lower_soc, ‘r-’, ‘LineWidth’, 2);
plot(time_vec, P_real_time_upper_soc, ‘y-’, ‘LineWidth’, 2);
plot(time_vec, P_real_time_lower_soc, ‘m-’, ‘LineWidth’, 2);
xlabel(‘时间’);
ylabel(‘容量(kWh)’);
title(‘日前与实时SOC上下界’);
legend(‘日前SOC上界’, ‘日前SOC下界’, ‘实时SOC上界’, ‘实时SOC下界’);
grid on;
tight_layout;
🔧 辅助函数 1:生成 EV 场景
matlab
function ev_list = generate_ev_scenarios(N, type, T, dt)
% 生成 N 辆 EV 的接入信息
ev_list = struct();
for i = 1:N
if strcmp(type, ‘day’)
t_arrive = 8 + rand2; % 8-10点到达
t_depart = 16 + rand2; % 16-18点离开
else
t_arrive = 18 + rand4; % 18-22点到达
t_depart = 6 + rand2; % 6-8点离开(跨天)
end
soc0 = SOC_min + (SOC_max - SOC_min) rand;
t_arrive_idx = floor(t_arrive / dt) + 1;
t_depart_idx = floor(t_depart / dt) + 1;
t_arrive_idx = max(1, min(T, t_arrive_idx));
t_depart_idx = max(1, min(T, t_depart_idx));
ev_list(i).t_arrive = t_arrive_idx;
ev_list(i).t_depart = t_depart_idx;
ev_list(i).soc0 = soc0;
end
end
🔧 辅助函数 2:聚合功率限值
matlab
function [P_up, P_low] = aggregate_power_limits(ev_list, T, Pch_max, Pdis_max, E_bat, eta, SOC_min, SOC_max, dt)
P_up = zeros(1, T);
P_low = zeros(1, T);
for t = 1:T
p_up_sum = 0;
p_low_sum = 0;
for i = 1:length(ev_list)
ev = ev_list(i);
if t >= ev.t_arrive && t <= ev.t_depart
[p_max, p_min] = compute_single_power_limit(ev.t_arrive, ev.t_depart, ev.soc0, t, Pch_max, Pdis_max, …
E_bat, eta, SOC_min, SOC_max, dt);
p_up_sum = p_up_sum + p_max;
p_low_sum = p_low_sum + p_min;
end
end
P_up(t) = p_up_sum;
P_low(t) = p_low_sum;
end
end
🔧 辅助函数 3:单 EV 功率限值(简化)
matlab
function [p_max, p_min] = compute_single_power_limit(t_arrive, t_depart, soc0, t, Pch_max, Pdis_max, E_bat, eta, SOC_min, SOC_max, dt)
% 简化:最大充电受剩余时间和 SOC 限制
E_can_charge = (SOC_max - soc0) E_bat;
E_can_discharge = (soc0 - SOC_min) E_bat;
p_max = min(Pch_max, E_can_charge / ((t_depart - t) dt) / eta);
p_min = max(Pdis_max, -E_can_discharge / ((t_depart - t) dt) eta);
end
🔧 辅助函数 4:模拟 SOC 轨迹(用于粒子滤波)
matlab
function soc_traj = simulate_soc_trajectory(ev, T, Pch_max, Pdis_max, E_bat, eta, SOC_min, SOC_max, dt)
% 简单模拟:假设以恒定速率充电
soc_traj = zeros(1, T);
soc = ev.soc0;
for t = 1:T
if t >= ev.t_arrive && t <= ev.t_depart
p = Pch_max (t < ev.t_depart); % 充电
soc = soc + p dt / E_bat;
soc = max(SOC_min, min(SOC_max, soc));
end
soc_traj(t) = soc;
end
end
✅ 输出结果
+--------------------------------------------------+ 功率(kW) ┌─────────────────────────────────────────────┐ │ ┌───────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │