调整生成数据集以及参数标准化

feature/Model-20260625
lvjunjie 4 weeks ago
parent 955b2e19fe
commit 11833dc5a1

@ -59,6 +59,7 @@ namespace RunnerIO
<< ", C=" << p.wellboreC
<< ", phi=" << p.phi
<< ", h=" << p.h
<< ", Ct=" << p.Ct
<< ", Cf=" << p.Cf;
if (!p.timeQ.empty() && !p.q.empty() && p.timeQ.size() == p.q.size()) {
@ -86,7 +87,7 @@ namespace RunnerIO
std::cerr << "RunnerIO::readParamsBin: header read failed\n";
return false;
}
if (magic != MAGIC || ver != 1)
if (magic != MAGIC || (ver != 1 && ver != 2))
{
std::cerr << "RunnerIO::readParamsBin: magic/version mismatch\n";
return false;
@ -97,6 +98,12 @@ namespace RunnerIO
if (!readDouble(fs, out.wellboreC)) return false;
if (!readDouble(fs, out.phi)) return false;
if (!readDouble(fs, out.h)) return false;
out.hasCt = false;
out.Ct = 0.0;
if (ver >= 2) {
if (!readDouble(fs, out.Ct)) return false;
out.hasCt = true;
}
if (!readDouble(fs, out.Cf)) return false;
out.sectionIndex = 0;

@ -5,13 +5,15 @@
struct RunnerParams
{
// 6 params
// 7 params
double k;
double skin;
double wellboreC;
double phi;
double h;
double Ct;
double Cf;
bool hasCt;
// optional schedule extension
uint32_t sectionIndex; // wellFlowSectionIndex
@ -19,7 +21,7 @@ struct RunnerParams
std::vector<double> q; // m^3/d
RunnerParams()
: k(0), skin(0), wellboreC(0), phi(0), h(0), Cf(0), sectionIndex(0)
: k(0), skin(0), wellboreC(0), phi(0), h(0), Ct(0), Cf(0), hasCt(false), sectionIndex(0)
{}
};

@ -122,7 +122,7 @@ static void applySampledParamsAndMaybeOverrideRate(HX_NWTM_MODEL_INPUT& in, cons
std::fill(in.CS.S.begin(), in.CS.S.end(), p.skin);
std::fill(in.CS.C.begin(), in.CS.C.end(), p.wellboreC);
// 覆盖 k/phi/h/Cfresize + fill复用 capacity
// 覆盖 k/phi/h/Ct/Cfresize + fill复用 capacity
const size_t nCells = in.GRID.Trinodexy.size();
in.Base.k.resize(nCells);
in.Base.phi.resize(nCells);
@ -130,6 +130,7 @@ static void applySampledParamsAndMaybeOverrideRate(HX_NWTM_MODEL_INPUT& in, cons
std::fill(in.Base.k.begin(), in.Base.k.end(), p.k);
std::fill(in.Base.phi.begin(), in.Base.phi.end(), p.phi);
std::fill(in.Base.h.begin(), in.Base.h.end(), p.h);
if (p.hasCt) in.Base.Cti = p.Ct;
in.Base.Cf = p.Cf;
// 制度覆盖params.bin 带 schedule 才覆盖

@ -39,15 +39,16 @@ generation: # 数据集生成规模与随机性设置
max_fail_examples_per_reason: 50 # 每类失败原因最多保存的示例数量
params: # 储层与井筒物理参数采样设置
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h"] # 采样时采用对数尺度的物理参数
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h", "Ct"] # 采样时采用对数尺度的物理参数
ranges: # 各物理参数采样范围
k: [1.0e-4, 100.0] # 渗透率采样范围
skin: [-10.0, 10.0] # 表皮系数采样范围
wellboreC: [1.0e-4, 2.0] # 井筒储集系数采样范围
phi: [1.0e-2, 0.50] # 孔隙度采样范围
h: [2.0, 100.0] # 储层厚度采样范围
Ct: [1.0e-4, 1.0] # 综合压缩系数采样范围
Cf: [1.0e-6, 5.0e-3] # 岩石压缩系数采样范围
fixed_params: # 固定不参与采样的物理参数
Cf:

@ -39,15 +39,16 @@ generation: # 数据集生成规模与随机性设置
max_fail_examples_per_reason: 50 # 每类失败原因最多保存的示例数量
params: # 储层与井筒物理参数采样设置
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h"] # 采样时采用对数尺度的物理参数
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h", "Ct"] # 采样时采用对数尺度的物理参数
ranges: # 各物理参数采样范围
k: [1.0e-4, 100.0] # 渗透率采样范围
skin: [-10.0, 10.0] # 表皮系数采样范围
wellboreC: [1.0e-4, 2.0] # 井筒储集系数采样范围
phi: [1.0e-2, 0.50] # 孔隙度采样范围
h: [2.0, 100.0] # 储层厚度采样范围
Ct: [1.0e-4, 1.0] # 综合压缩系数采样范围
Cf: [1.0e-6, 5.0e-3] # 岩石压缩系数采样范围
fixed_params: # 固定不参与采样的物理参数
Cf:

@ -39,15 +39,16 @@ generation: # 数据集生成规模与随机性设置
max_fail_examples_per_reason: 50 # 每类失败原因最多保存的示例数量
params: # 储层与井筒物理参数采样设置
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h"] # 采样时采用对数尺度的物理参数
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h", "Ct"] # 采样时采用对数尺度的物理参数
ranges: # 各物理参数采样范围
k: [1.0e-4, 100.0] # 渗透率采样范围
skin: [-10.0, 10.0] # 表皮系数采样范围
wellboreC: [1.0e-4, 2.0] # 井筒储集系数采样范围
phi: [1.0e-2, 0.50] # 孔隙度采样范围
h: [2.0, 100.0] # 储层厚度采样范围
Ct: [1.0e-4, 1.0] # 综合压缩系数采样范围
Cf: [1.0e-6, 5.0e-3] # 岩石压缩系数采样范围
fixed_params: # 固定不参与采样的物理参数
Cf:

@ -39,15 +39,16 @@ generation: # 数据集生成规模与随机性设置
max_fail_examples_per_reason: 50 # 每类失败原因最多保存的示例数量
params: # 储层与井筒物理参数采样设置
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h"] # 采样时采用对数尺度的物理参数
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h", "Ct"] # 采样时采用对数尺度的物理参数
ranges: # 各物理参数采样范围
k: [1.0e-4, 100.0] # 渗透率采样范围
skin: [-10.0, 10.0] # 表皮系数采样范围
wellboreC: [1.0e-4, 2.0] # 井筒储集系数采样范围
phi: [1.0e-2, 0.50] # 孔隙度采样范围
h: [2.0, 100.0] # 储层厚度采样范围
Ct: [1.0e-4, 1.0] # 综合压缩系数采样范围
Cf: [1.0e-6, 5.0e-3] # 岩石压缩系数采样范围
fixed_params: # 固定不参与采样的物理参数
Cf:

@ -39,15 +39,16 @@ generation: # 数据集生成规模与随机性设置
max_fail_examples_per_reason: 50 # 每类失败原因最多保存的示例数量
params: # 储层与井筒物理参数采样设置
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h"] # 采样时采用对数尺度的物理参数
all_physical_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"] # 写入数据集的完整物理参数列表
active_param_names: ["k", "skin", "wellboreC", "phi", "h", "Ct"] # 参与采样变化的物理参数列表
log_params: ["k", "wellboreC", "h", "Ct"] # 采样时采用对数尺度的物理参数
ranges: # 各物理参数采样范围
k: [1.0e-3, 10.0] # 渗透率采样范围
skin: [-10.0, 10.0] # 表皮系数采样范围
wellboreC: [1.0e-4, 2.0] # 井筒储集系数采样范围
phi: [1.0e-2, 0.50] # 孔隙度采样范围
h: [2.0, 50.0] # 储层厚度采样范围
Ct: [1.0e-4, 1.0] # 综合压缩系数采样范围
Cf: [1.0e-6, 5.0e-3] # 岩石压缩系数采样范围
fixed_params: # 固定不参与采样的物理参数
Cf:

@ -89,6 +89,7 @@ def main() -> None:
else np.full((len(neighbor_anchor_id),), np.nan, dtype=np.float32)
)
param_names = _decode_attr_list(src.attrs.get("param_names"))
schedule_meta_names = _decode_attr_list(src.attrs.get("schedule_meta_names"))
span_fracs = (
np.asarray(
@ -114,6 +115,8 @@ def main() -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
with h5py.File(output_path, "w") as dst:
if param_names is not None:
dst.attrs["param_names"] = np.asarray(param_names, dtype="S")
if schedule_meta_names is not None:
dst.attrs["schedule_meta_names"] = np.asarray(schedule_meta_names, dtype="S")
if span_fracs:

@ -411,7 +411,7 @@ def run_solver_and_extract_curve(
def params_to_array(params: Params) -> np.ndarray:
"""按固定参数顺序把 Params 对象转换成数值数组。"""
return np.asarray(
[params.k, params.skin, params.wellboreC, params.phi, params.h, params.Cf],
[params.k, params.skin, params.wellboreC, params.phi, params.h, params.Ct, params.Cf],
dtype=np.float32,
)
@ -493,6 +493,7 @@ def sample_neighbor_params(
"wellboreC": float(base.wellboreC),
"phi": float(base.phi),
"h": float(base.h),
"Ct": float(base.Ct),
"Cf": float(base.Cf),
}
@ -505,6 +506,7 @@ def sample_neighbor_params(
"wellboreC": 0.75,
"phi": 0.60,
"h": 0.75,
"Ct": 0.75,
"Cf": 0.40,
}
cand = dict(base_dict)
@ -550,6 +552,7 @@ def sample_neighbor_params(
wellboreC=cand["wellboreC"],
phi=cand["phi"],
h=cand["h"],
Ct=cand["Ct"],
Cf=cand["Cf"],
schedule=base.schedule,
)
@ -664,7 +667,7 @@ def create_output_file(
"""创建邻域 HDF5 文件,并初始化锚点、候选、曲线和元数据数据集。"""
output_path.parent.mkdir(parents=True, exist_ok=True)
f = h5py.File(output_path, "w")
f.attrs["param_names"] = np.asarray(["k", "skin", "wellboreC", "phi", "h", "Cf"], dtype="S")
f.attrs["param_names"] = np.asarray(["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"], dtype="S")
f.attrs["schedule_meta_names"] = np.asarray(SCHEDULE_META_NAMES, dtype="S")
f.attrs["span_fracs"] = np.asarray(span_fracs, dtype=np.float32)
f.attrs["search_param_names"] = np.asarray(search_names, dtype="S")

@ -415,6 +415,7 @@ def _worker_simulate_parallel(args):
wellboreC=float(params_dict["wellboreC"]),
phi=float(params_dict["phi"]),
h=float(params_dict["h"]),
Ct=float(params_dict.get("Ct", 1.0e-3)),
Cf=float(params_dict["Cf"]),
schedule=None,
)
@ -486,7 +487,7 @@ def _worker_simulate_parallel(args):
)
params_vec = np.asarray(
[p.k, p.skin, p.wellboreC, p.phi, p.h, p.Cf],
[float(getattr(p, name)) for name in cfg.raw["params"]["all_physical_param_names"]],
dtype=np.float32,
)
@ -537,6 +538,7 @@ class HDF5Appender:
self,
filepath: Path,
param_dim: int,
param_names: list[str],
schedule_dim: int,
curve_dim: int,
compression=None,
@ -547,12 +549,14 @@ class HDF5Appender:
self.filepath.parent.mkdir(parents=True, exist_ok=True)
self.param_dim = int(param_dim)
self.param_names = list(param_names)
self.schedule_dim = int(schedule_dim)
self.curve_dim = int(curve_dim)
self.time_dim = int(curve_dim) // 2
self.f = h5py.File(self.filepath, "w")
self._n = 0
self.f.attrs["param_names"] = np.asarray(self.param_names, dtype="S")
self.f.attrs["schedule_meta_names"] = np.asarray(SCHEDULE_META_NAMES, dtype="S")
def ds(name, shape_tail, dtype, fillvalue=None):
@ -714,7 +718,8 @@ class ParallelDatasetGenerator:
sched_dim = n_u_points * n_channels + cfg.sec_feat_dim
if bool(cfg.raw.get("schedule", {}).get("use_metadata_features_for_model", False)):
sched_dim += len(cfg.raw.get("schedule", {}).get("metadata_features_for_model", []) or [])
param_dim = 6
param_names = list(cfg.raw["params"]["all_physical_param_names"])
param_dim = len(param_names)
# HDF5 布局与正演模型输入输出一致:
# params/schedule 是输入curve 是数值求解器生成的监督目标。
@ -725,6 +730,7 @@ class ParallelDatasetGenerator:
app = HDF5Appender(
filepath=filepath,
param_dim=param_dim,
param_names=param_names,
schedule_dim=sched_dim,
curve_dim=curve_dim,
compression=compression,

@ -20,8 +20,8 @@ import numpy as np
# 部分物理参数跨越多个数量级,因此先做特征变换,再交给模型学习。
DEFAULT_PARAM_NAMES = ["k", "skin", "wellboreC", "phi", "h", "Cf"]
DEFAULT_LOG_PARAM_NAMES = {"k", "wellboreC", "h"}
DEFAULT_PARAM_NAMES = ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"]
DEFAULT_LOG_PARAM_NAMES = {"k", "wellboreC", "h", "Ct"}
DEFAULT_ASINH_PARAM_NAMES = {"skin"}
DEFAULT_COMPOSITE_FEATURES = [
"log10_kh",

@ -73,7 +73,8 @@ class Params:
wellboreC: float
phi: float
h: float
Cf: float
Ct: float = 1.0e-3
Cf: float = 4.315e-4
schedule: Optional[Schedule] = None
def to_dict(self) -> Dict[str, float]:
@ -83,8 +84,8 @@ class Params:
def to_bin_bytes(self, cfg: Config, include_schedule: Optional[bool] = None) -> bytes:
"""按 C++ 求解器约定把参数和可选流量制度编码为 params.bin 内容。
二进制头部固定为 PRM1 magic + version随后写入 6 double 物理参数
kskinwellboreCphihCf include_schedule=True则继续写入
二进制头部固定为 PRM1 magic + version随后写入 7 double 物理参数
kskinwellboreCphihCtCf include_schedule=True则继续写入
sectionIndex分段数量 nQtimeQ 数组和 q 数组
这里的字段顺序字节序和数据类型必须与 C++ runner 完全一致否则求解器会
@ -94,12 +95,12 @@ class Params:
include_schedule = bool(cfg.get("schedule", "write_schedule_to_params_bin", default=False))
magic = ord("P") | (ord("R") << 8) | (ord("M") << 16) | (ord("1") << 24)
version = 1
version = 2
b = struct.pack(
"<II6d",
"<II7d",
int(magic), int(version),
float(self.k), float(self.skin), float(self.wellboreC),
float(self.phi), float(self.h), float(self.Cf),
float(self.phi), float(self.h), float(self.Ct), float(self.Cf),
)
if include_schedule and self.schedule is not None:
@ -263,7 +264,7 @@ def generate_params_dataset(cfg: Config, n_samples: int, method: str | None = No
"""批量生成物理参数样本。
采样只直接作用于 active_param_names未激活的参数由 fixed_params 或全局范围补齐
从而保证写给 C++ 求解器的 Params 始终包含完整 6 个字段函数会对 active 参数
从而保证写给 C++ 求解器的 Params 始终包含完整 7 个字段函数会对 active 参数
组合做简单去重避免 Sobol 截断或固定参数导致重复样本进入数据集
"""
method = (method or cfg.raw["params"].get("sampling_method", "sobol")).lower()

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""原始 HDF5 样本预处理。
数据生成阶段写出的 HDF5 文件仍处在原始样本状态参数尚未做特征变换输入和
@ -186,6 +186,11 @@ def preprocess_dataset(
param_names = _decode_attr_list(f.attrs.get("param_names"))
schedule_meta_names = _decode_attr_list(f.attrs.get("schedule_meta_names"))
source_name_vocab = _decode_attr_list(f.attrs.get("source_name_vocab"))
if param_names is None:
if x_params.shape[1] == 6:
param_names = ["k", "skin", "wellboreC", "phi", "h", "Cf"]
elif x_params.shape[1] == 7:
param_names = ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"]
schedule_meta = _ensure_2d("schedule_meta", f["schedule_meta"][:]) if "schedule_meta" in f else None
family_name = _read_optional_string_dataset(f, "family_name")

@ -887,7 +887,7 @@ void nmCalculationAutoFitPSO::closeTraceFile()
void nmCalculationAutoFitPSO::writeTraceHeader()
{
// trace CSV 字段说明:
// - 当前粒子参数只记录代理模型关心的 k/skin/wellboreC/phi/h/Cf
// - 当前粒子参数只记录代理模型关心的 k/skin/wellboreC/phi/h/Ct/Cf
// - solver_objective 是真实求解器误差;
// - surrogate_objective 是 Python 代理评分;
// - screening_decision 说明该粒子为什么跑/不跑真实求解器;
@ -906,6 +906,7 @@ void nmCalculationAutoFitPSO::writeTraceHeader()
<< "wellboreC"
<< "phi"
<< "h"
<< "Ct"
<< "Cf"
<< "solver_objective"
<< "solver_success"
@ -918,6 +919,7 @@ void nmCalculationAutoFitPSO::writeTraceHeader()
<< "pbest_wellboreC"
<< "pbest_phi"
<< "pbest_h"
<< "pbest_Ct"
<< "pbest_Cf"
<< "gbest_objective"
<< "gbest_k"
@ -925,6 +927,7 @@ void nmCalculationAutoFitPSO::writeTraceHeader()
<< "gbest_wellboreC"
<< "gbest_phi"
<< "gbest_h"
<< "gbest_Ct"
<< "gbest_Cf"
<< "enabled_param_indices";
@ -1101,6 +1104,7 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation,
<< traceParamAt(currentParams, 2)
<< traceParamAt(currentParams, 3)
<< traceParamAt(currentParams, 5)
<< traceParamAt(currentParams, 6)
<< traceParamAt(currentParams, 7)
<< traceNumber(solverObjective)
<< QString::number(solverSuccess ? 1 : 0)
@ -1113,6 +1117,7 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation,
<< traceParamAt(pbestParams, 2)
<< traceParamAt(pbestParams, 3)
<< traceParamAt(pbestParams, 5)
<< traceParamAt(pbestParams, 6)
<< traceParamAt(pbestParams, 7)
<< traceNumber(m_globalBestFitness)
<< traceParamAt(gbestParams, 0)
@ -1120,6 +1125,7 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation,
<< traceParamAt(gbestParams, 2)
<< traceParamAt(gbestParams, 3)
<< traceParamAt(gbestParams, 5)
<< traceParamAt(gbestParams, 6)
<< traceParamAt(gbestParams, 7)
<< csvEscape(enabledIndices.join(";"));
@ -1272,7 +1278,7 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat
//
// 文件位置通常是 ML/nmWTAI-ML/data/temp/pso_screen_candidates_<run_id>_gen<N>.csv。
// 字段顺序必须与 ML 脚本 score_pso_candidates*.py 保持一致:
// particle_id,k,skin,wellboreC,phi,h,Cf
// particle_id,k,skin,wellboreC,phi,h,Ct,Cf
//
// 这里写的是“完整参数体系”中的关键字段,不是粒子内部紧凑向量。
// 例如用户没有勾选 Cf 时Cf 会从当前 DataManager 取值写入 CSV。
@ -1285,7 +1291,7 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat
QTextStream out(&file);
// 代理模型只需要这些输入字段;其它参数当前不在训练输入集中。
out << "particle_id,k,skin,wellboreC,phi,h,Cf\n";
out << "particle_id,k,skin,wellboreC,phi,h,Ct,Cf\n";
for(int i = 0; i < m_swarm.size(); ++i) {
QVector<double> params = buildTraceParameterVector(m_swarm[i].position);
@ -1296,6 +1302,7 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat
<< traceParamAt(params, 2)
<< traceParamAt(params, 3)
<< traceParamAt(params, 5)
<< traceParamAt(params, 6)
<< traceParamAt(params, 7);
out << cols.join(",") << "\n";
}
@ -1893,10 +1900,10 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co
return false;
}
// 参数 gate代理输入目前只覆盖 k/skin/wellboreC/phi/h
// 参数 gate代理输入目前只覆盖 k/skin/wellboreC/phi/h/Ct
// 用户勾选其它参数时,代理无法可靠反映这些参数变化,直接禁用代理筛选。
QVector<int> allowedParamIndices;
allowedParamIndices << 0 << 1 << 2 << 3 << 5;
allowedParamIndices << 0 << 1 << 2 << 3 << 5 << 6;
for(int i = 0; i < m_enabledParamIndices.size(); ++i) {
if(!allowedParamIndices.contains(m_enabledParamIndices[i])) {

@ -9,7 +9,7 @@ nmDataAutomaticFitting::nmDataAutomaticFitting()
m_porositySelected = true; // 默认选中
m_initialPressureSelected = false; // 默认不选中
m_thicknessSelected = true; // 默认选中
m_ctSelected = false; // 默认不选中
m_ctSelected = true; // 默认选中
m_cfSelected = false; // 默认不选中
m_swiSelected = false; // 默认不选中

Loading…
Cancel
Save