1、独立计算模块;

feature/ribbon-menu-20240927
simonyan 4 days ago
parent 5ceefee4a8
commit 70b7bdd7c1

7
.gitignore vendored

@ -35,4 +35,11 @@ Src4/.qmake.stash
Src4/nmWTAI.pro*
Src4/Makefile
Src4/nmNum/Makefile
Src4/nmNum/nmCalculation/Win32
Src4/nmNum/nmCalculation/debug
Src4/nmNum/nmCalculation/release
Src4/nmNum/nmCalculation/Makefile*
Src4/nmNum/nmCalculation/*.pro.user
Src4/nmNum/nmCalculation/*.vcxproj*
Src4/nmNum/nmCalculation/*.pdb
Bin-versions

@ -0,0 +1,12 @@
#ifndef NMCALCULATION_H
#define NMCALCULATION_H
#include "nmCalculation_global.h"
class NMCALCULATION_EXPORT NmCalculation
{
public:
NmCalculation();
};
#endif // NMCALCULATION_H

@ -0,0 +1,6 @@
#ifndef NMCALCULATIONDEFINE_H
#define NMCALCULATIONDEFINE_H
#endif // NMCALCULATIONDEFINE_H

@ -0,0 +1,26 @@
#ifndef NMCALCULATIONGEO_H
#define NMCALCULATIONGEO_H
#include <QString>
#include <QPointF>
#include "nmCalculationGeoDataOutline.h"
#include "nmCalculationGeoDataWell.h"
class nmCalculationGeo
{
public:
// nmCalculationGeo();
// 构造函数
nmCalculationGeo(const nmCalculationGeoDataOutline &outline, const QVector<nmCalculationGeoDataWell>& wellList, double lc);
void generate2DFile(const QString& outputFileName);
void generate3DFile(const QString& outputFileName);
private:
QString generateOCCGeoCodes(bool is3D = true);
//基础网格大小设置
nmCalculationGeoDataOutline m_outline;//存储多边形的点
QVector<nmCalculationGeoDataWell> m_vWellList; // 存储圆心数据
double m_dGridSize;// 基础网格大小设置
};
#endif // NMCALCULATIONGEO_H

@ -0,0 +1,18 @@
#ifndef NMCALCULATIONGEODATAOUTLINE_H
#define NMCALCULATIONGEODATAOUTLINE_H
#include <QPointF>
#include <QVector>
class nmCalculationGeoDataOutline
{
public:
nmCalculationGeoDataOutline(const QVector<QPointF> points);
const QVector<QPointF> &points() const;
void setPoints(const QVector<QPointF> &newVPoints);
private:
QVector<QPointF> m_vPoints;
};
#endif // NMCALCULATIONGEODATAOUTLINE_H

@ -0,0 +1,32 @@
#ifndef NMCALCULATIONGEODATAWELL_H
#define NMCALCULATIONGEODATAWELL_H
#include <QString>
#include <QVector>
#include <QPointF>
class nmCalculationGeoDataWell
{
public:
// 默认构造函数
nmCalculationGeoDataWell();
// 带参数构造函数
nmCalculationGeoDataWell(const QPointF& center, double radius, int roundPointCount);
// 拷贝构造函数
nmCalculationGeoDataWell(const nmCalculationGeoDataWell& other);
// 重载赋值运算符
nmCalculationGeoDataWell& operator=(const nmCalculationGeoDataWell& other);
QPointF center() const;
void setCenter(QPointF newCenter);
double radius() const;
void setRadius(double newDRadius);
int roundPointCount() const;
void setRoundPointCount(int newIRoundPointCount);
public:
QPointF m_center; // 圆心位置
double m_dRadius; // 半径
int m_iRoundPointCount; // 圆周网格数量,用于加密
};
#endif // NMCALCULATIONGEODATAWELL_H

@ -0,0 +1,18 @@
#ifndef NMCALCULATIONGEOSURFACE_H
#define NMCALCULATIONGEOSURFACE_H
#include <QString>
#include "nmCalculationGeoDataOutline.h"
class nmCalculationGeoSurface
{
public:
nmCalculationGeoSurface(const nmCalculationGeoDataOutline& outline, const double& gridSize = 0.1);
QString generateSurfaceGeoCodes();
private:
nmCalculationGeoDataOutline m_outline;
double m_dGridSize;// 基础网格大小设置
};
#endif // NMCALCULATIONGEOSURFACE_H

@ -0,0 +1,19 @@
#ifndef NMCALCULATIONGEOWELL_H
#define NMCALCULATIONGEOWELL_H
#include <QString>
#include <QVector>
#include "nmCalculationGeoDataWell.h"
class nmCalculationGeoWell
{
public:
nmCalculationGeoWell(const QVector<nmCalculationGeoDataWell>& wellList);
//创建井的方法
QString generateWellGeoCodes(int index);
private:
QVector<nmCalculationGeoDataWell> m_vWellList;
};
#endif // NMCALCULATIONGEOWELL_H

@ -0,0 +1,11 @@
#ifndef NMCALCULATIONGRID_H
#define NMCALCULATIONGRID_H
class nmCalculationGrid
{
public:
nmCalculationGrid();
};
#endif // NMCALCULATIONGRID_H

@ -0,0 +1,30 @@
#ifndef NMCALCULATIONSOLVER_H
#define NMCALCULATIONSOLVER_H
#include <QObject>
#include <iostream>
#include "nmCalculation_global.h"
#include "singlePhaseSolver.h"
using namespace std;
class CWell;
class NMCALCULATION_EXPORT nmCalculationSolver
{
public:
nmCalculationSolver();
// 执行求解,需要 vtk文件井的信息包括每个井的圆心、半径
bool execSolve(QString vtkFilePath, QString reservoirParamFilePath, QString wellsParamFilePath, QString dllDir, QString outputFile);
private:
// 为求解器读取所有需要的数据
void readVTK(QString vtkFlePath);
// 读取网格信息包括点、cell等信息
bool readMesh(std::string fname, std::vector<Point> &points, std::vector<Cell> &cells);
// 读取油藏属性
bool readReservoirParamters(std::string fname, double* pBaseData);
// 读取井的信息,包括:文件名、几口井、每口井的物理参数
bool readWells(std::string fname, int& numWell, CWell* well);
};
#endif // NMCALCULATIONSOLVER_H

@ -0,0 +1,12 @@
#ifndef NMCALCULATION_GLOBAL_H
#define NMCALCULATION_GLOBAL_H
#include <QtCore/qglobal.h>
#if defined(NM_CALCULATION_LIBRARY)
#define NMCALCULATION_EXPORT Q_DECL_EXPORT
#else
#define NMCALCULATION_EXPORT Q_DECL_IMPORT
#endif
#endif // NMCALCULATION_GLOBAL_H

@ -19,6 +19,8 @@ public:
// 构造函数
nmDataGeo(const QVector<QPointF>&points, const QVector<CircleWell>& circlesPoint, double lc);
void generate2DFile(const QString& fileName);
// 写井的信息,包括:井的圆心,半径
void generateWells(const QString& fileName);
void generate3DFile(const QString& fileName);
private:
QString createFile(bool is3D = true);

@ -0,0 +1,5 @@
#include "nmCalculation.h"
NmCalculation::NmCalculation()
{
}

@ -0,0 +1,64 @@
#include "nmCalculationGeo.h"
#include <QFile>
#include <QTextStream>
#include <QStringList>
#include "nmCalculationGeoSurface.h"
#include "nmCalculationGeoWell.h"
nmCalculationGeo::nmCalculationGeo(const nmCalculationGeoDataOutline &outline, const QVector<nmCalculationGeoDataWell>& wellList, double lc): m_outline(outline),
m_vWellList(wellList), m_dGridSize(lc)
{
}
QString nmCalculationGeo::generateOCCGeoCodes(bool is3D)
{
int flag = 0;
QStringList geoLines;
// 生成线、面
nmCalculationGeoSurface surface(m_outline, m_dGridSize);
geoLines.append(surface.generateSurfaceGeoCodes());
// 对于井做处理
flag = flag + m_vWellList.size();
nmCalculationGeoWell wells(m_vWellList);
geoLines.append(wells.generateWellGeoCodes(flag));
geoLines.append("Transfinite Surface(1) = {1}; ");
if (is3D) {
// 设置网格厚度,目前只支持单层网格
double gridThickness = 0.2;
geoLines.append(QString("Extrude {0, 0, %1} {Surface{1};Layers{1};Recombine;}").arg(gridThickness));
}
return geoLines.join("\n");
}
void nmCalculationGeo::generate2DFile(const QString& fileName)
{
QFile file(fileName);
//检查
if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning("Cannot open file for writing: %s", qPrintable(file.errorString()));
return;
}
QTextStream out(&file);
out << generateOCCGeoCodes(false);
file.close();
}
void nmCalculationGeo::generate3DFile(const QString& fileName)
{
QFile file(fileName);
//检查
if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning("Cannot open file for writing: %s", qPrintable(file.errorString()));
return;
}
QTextStream out(&file);
out << generateOCCGeoCodes();
file.close();
}

@ -0,0 +1,15 @@
#include "nmCalculationGeoDataOutline.h"
nmCalculationGeoDataOutline::nmCalculationGeoDataOutline(const QVector<QPointF> points): m_vPoints(points)
{
}
const QVector<QPointF> &nmCalculationGeoDataOutline::points() const
{
return m_vPoints;
}
void nmCalculationGeoDataOutline::setPoints(const QVector<QPointF> &newVPoints)
{
m_vPoints = newVPoints;
}

@ -0,0 +1,56 @@
#include "nmCalculationGeoDataWell.h"
nmCalculationGeoDataWell::nmCalculationGeoDataWell()
{
}
nmCalculationGeoDataWell::nmCalculationGeoDataWell(const QPointF& center, double radius, int roundPointCount)
: m_center(center), m_dRadius(radius), m_iRoundPointCount(roundPointCount) {}
nmCalculationGeoDataWell::nmCalculationGeoDataWell(const nmCalculationGeoDataWell& other)
{
m_center = other.m_center;
m_dRadius = other.m_dRadius;
m_iRoundPointCount = other.m_iRoundPointCount;
}
nmCalculationGeoDataWell& nmCalculationGeoDataWell::operator=(const nmCalculationGeoDataWell& other)
{
if (this != &other) {
m_center = other.m_center;
m_dRadius = other.m_dRadius;
m_iRoundPointCount = other.m_iRoundPointCount;
}
return *this;
}
int nmCalculationGeoDataWell::roundPointCount() const
{
return m_iRoundPointCount;
}
void nmCalculationGeoDataWell::setRoundPointCount(int newIRoundPointCount)
{
m_iRoundPointCount = newIRoundPointCount;
}
double nmCalculationGeoDataWell::radius() const
{
return m_dRadius;
}
void nmCalculationGeoDataWell::setRadius(double newDRadius)
{
m_dRadius = newDRadius;
}
QPointF nmCalculationGeoDataWell::center() const
{
return m_center;
}
void nmCalculationGeoDataWell::setCenter(QPointF newCenter)
{
m_center = newCenter;
}

@ -0,0 +1,50 @@
#include "nmCalculationGeoSurface.h"
#include <QStringList>
nmCalculationGeoSurface::nmCalculationGeoSurface(const nmCalculationGeoDataOutline& outline, const double& gridSize): m_outline(outline), m_dGridSize(gridSize)
{
}
//面的文件部分的创造
QString nmCalculationGeoSurface::generateSurfaceGeoCodes()
{
QStringList geoLines;
QVector<QPointF> points = m_outline.points();
geoLines.append(QString("SetFactory(\"OpenCASCADE\");"));//加入OCC
for(int i = 0; i < points.size(); ++i) {
const QPointF& point = points[i];
//点point部分多边形相关点的坐标以及网格划分的基础网格大小
geoLines.append(QString("Point(%1)={%2,%3,0,%4};")
.arg(i + 1)
.arg(point.x())
.arg(point.y())
.arg(m_dGridSize));
}
for(int i = 0; i < points.size(); i++) {
//线:规定多边形的两点连接成线
geoLines.append(QString("Line(%1)={%2,%3};")
.arg(i + 1)
.arg(i + 1)
.arg((i + 1) % points.size() + 1));
}
QStringList pointIndexList;
//面:多线封闭连接成面
pointIndexList.append("Line Loop(1) = {");
for (int i = 1; i <= points.size(); ++i) {
pointIndexList.append(QString::number(i));
if (i < points.size()) {
pointIndexList.append(",");
}
}
pointIndexList.append("};");
geoLines.append(pointIndexList.join(""));
geoLines.append("Plane Surface(1) = {1};");//面:单独的面
return geoLines.join("\n");
}

@ -0,0 +1,41 @@
#include "nmCalculationGeoWell.h"
#include <QStringList>
nmCalculationGeoWell::nmCalculationGeoWell(const QVector<nmCalculationGeoDataWell>& wellList): m_vWellList(wellList)
{
}
QString nmCalculationGeoWell::generateWellGeoCodes(int index)
{
QStringList circleLines;
// 单独的 Sphere 的文件内容
for (int i = 0; i < m_vWellList.size(); i++) {
const nmCalculationGeoDataWell & well = m_vWellList[i];
circleLines.append(QString("Sphere(%1) = {%2, %3, 0, %4};")
.arg(i + 1) // 编号
.arg(well.center().x()) // x坐标
.arg(well.center().y()) // y坐标
.arg(well.radius())); // 圆的半径
}
// 添加布尔差异的定义
// qDebug()<<m_circles.size()<<"!!!!!!";
for (int i = 0; i < m_vWellList.size(); ++i) {
//这里是抠图部分注意surface是平面这里我们只存在一个底面因此surface不变
circleLines.append(QString("BooleanDifference{ Surface{%1}; Delete; }{ Volume{%2}; Delete; }")
.arg(1) // Surface{1},这里可以根据多边形的数量进行调整
.arg(i + 1)); // Volume{1},递增数字
}
//圆周分段:自定义的圆周网格个数,确保达到密集化效果
for(int i = 0; i < m_vWellList.size(); i++) {
const nmCalculationGeoDataWell & well = m_vWellList[i];
circleLines.append(QString("Transfinite Curve {%1} = %2;")
.arg(index + i + 1)
.arg(well.roundPointCount()));
}
return circleLines.join("\n");
}

@ -0,0 +1,6 @@
#include "nmCalculationGrid.h"
nmCalculationGrid::nmCalculationGrid()
{
}

@ -0,0 +1,205 @@
#include "nmCalculationSolver.h"
#include <windows.h>
#include <fstream>
#include <sstream>
#include <QLibrary>
#include <QDebug>
nmCalculationSolver::nmCalculationSolver()
{
}
bool nmCalculationSolver::execSolve(QString vtkFilePath, QString reservoirParamFilePath, QString wellsParamFilePath, QString dllDir, QString outputFile)
{
// 读取点、cell信息
std::vector<Point> points;
std::vector<Cell> cells;
this->readMesh(vtkFilePath.toStdString(), points, cells);
// 读取油藏信息
double *pBaseData;
pBaseData = new double[8];
this->readReservoirParamters(reservoirParamFilePath.toStdString(), pBaseData);
// 读取井的属性信息
int nWellNum;
CWell* wWell = NULL;
this->readWells(wellsParamFilePath.toStdString(), nWellNum, wWell);
QString dllPath = dllDir + "singlePhaseSolverDll.dll";
//LPCWSTR name;
//wchar_t* str2 = new wchar_t[dllPath.size() + 1];
//int len1 = dllPath.toWCharArray(str2);
//str2[len1] = '\0';//不添加这个,会有乱码
//name = str2;
// HMODULE hMod = LoadLibrary(name);
//HMODULE hMod = LoadLibrary(L"E:\01-Projects\16-CNPC\16-nmWTAI-gitea\Bin\Temp\Nm\Solver\singlePhaseSolverDll.dll");
//int err = GetLastError();
// if (NULL == hMod) {
// std::cout << "singlePhaseSolver.dll加载失败\n";
// delete[] pBaseData;
// delete[] wWell;
// return false;
// }
// typedef int(*Solver)(double*, std::vector<Point>, std::vector<Cell>, int, CWell*,
// std::vector<std::vector<Point >> &, std::vector<std::pair<double, std::vector<double >>> &);
// // Get the address of the trinomial function
// Solver solverFun = (Solver)GetProcAddress(hMod, "singlePhaseSolver");
// if (NULL == solverFun) {
// FreeLibrary(hMod);
// std::cout << "singlePhaseSolver文件加载函数地址获取失败\n";
// delete[] pBaseData;
// delete[] wWell;
// return false;
// }
QLibrary library(dllPath);
// 加载动态库:
if (library.load()) {
qDebug() << "Library loaded successfully.";
} else {
qDebug() << "Failed to load library:" << library.errorString();
}
// 获取函数指针并调用函数:
typedef int(*Solver)(double*, std::vector<Point>, std::vector<Cell>, int, CWell*,
std::vector<std::vector<Point >> &, std::vector<std::pair<double, std::vector<double >>> &);
Solver solverFun = (Solver)library.resolve("singlePhaseSolver");
if (solverFun) {
qDebug() << "Success to resolve function";
} else {
qDebug() << "Failed to resolve function:" << library.errorString();
}
std::vector<std::vector<Point >> vecBP;
std::vector<std::pair<double, std::vector<double >>> vecFieldPre;
// 调用求解器求解
solverFun(pBaseData, points, cells, nWellNum, wWell, vecBP, vecFieldPre);
// FreeLibrary(hMod);
delete[] pBaseData;
delete[] wWell;
return true;
}
bool nmCalculationSolver::readMesh(std::string fname, std::vector<Point> &points, std::vector<Cell> &cells)
{
std::ifstream file(fname);
if (!file.is_open()) {
return false;
}
// Skip header
std::string line;
for (int i = 0; i < 4; ++i) {
std::getline(file, line);
}
// Read points
std::getline(file, line);
std::istringstream iss(line);
std::string keyword;
int pointCount;
iss >> keyword >> pointCount;
points.resize(pointCount);
for (int i = 0; i < points.size(); i++) {
Point& point = points[i];
file >> point.x >> point.y >> point.z;
}
// Read cells
std::getline(file, line); // Skip empty line
std::getline(file, line);
std::getline(file, line);
iss = std::istringstream(line);
int cellCount, totalListSize;
iss >> keyword >> cellCount >> totalListSize;
cells.resize(cellCount);
// for (Cell& cell : cells) {
for (int i = 0; i < cells.size(); i++) {
Cell& cell = cells[i];
int cellPointSize;
file >> cellPointSize;
cell.pointIndices.resize(cellPointSize);
// for (int& index : cell.pointIndices) {
std::cout << cellPointSize;
for (int j = 0; j < cell.pointIndices.size(); j++) {
int& index = cell.pointIndices[j];
file >> index;
}
}
// Read cell types
std::getline(file, line); // Skip empty line
std::getline(file, line);
std::getline(file, line);
iss = std::istringstream(line);
iss >> keyword >> cellCount;
// for (Cell& cell : cells) {
for (int i = 0; i < cells.size(); i++) {
Cell& cell = cells[i];
file >> cell.type;
}
// Read point data
std::getline(file, line); // Skip empty line
std::getline(file, line);
std::getline(file, line);
iss = std::istringstream(line);
int dataCount;
iss >> keyword >> dataCount;
std::getline(file, line);
std::getline(file, line);
// for (Point& point : points) {
for (int i = 0; i < points.size(); i++) {
Point& point = points[i];
point.pointData.resize(1);
file >> point.pointData[0];
}
return 1;
}
bool nmCalculationSolver::readReservoirParamters(std::string fname, double* pBaseData)
{
std::ifstream file(fname);
if (!file.is_open()) {
return false;
}
for (int i = 0; i < 8; ++i) {
file >> pBaseData[i];
}
return true;
}
bool nmCalculationSolver::readWells(std::string fname, int& numWell, CWell* well)
{
std::fstream f(fname);
if (!f.is_open()) {
return false;
}
f >> numWell;
well = new CWell[numWell];
for (int i = 0; i < numWell; ++i) {
f >> well[i].dRc >> well[i].dSkin >> well[i].dC >> well[i].nTimeNumQ;
if (well[i].nTimeNumQ > 1) {
well[i].bisMultFlow = true;
}
for (int j = 0; j < well[i].nTimeNumQ; ++j) {
f >> well[i].pdTimeQ[j] >> well[i].pdQ[j];
}
}
return true;
}

@ -45,6 +45,28 @@ void nmDataGeo::generate2DFile(const QString& fileName)
file.close();
}
void nmDataGeo::generateWells(const QString &fileName)
{
QFile file(fileName);
//检查
if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning("Cannot open file for writing: %s", qPrintable(file.errorString()));
return;
}
QTextStream out(&file);
out << m_circlesPoint.size() << endl;
// 写井的信息
for (int i = 0; i < m_circlesPoint.size(); i++) {
CircleWell& well = m_circlesPoint[i];
out << well.center.x() << " " << well.center.y() << " " << well.r << " "<< well.pointCount <<endl;
}
file.close();
}
void nmDataGeo::generate3DFile(const QString& fileName)
{
QFile file(fileName);

@ -156,6 +156,7 @@ void nmSubWndGrid::genGeo(QVector<QPointF> outlinePoints, QVector<QVector<double
QString sFile = sDir + "oil.geo";
geo.generate3DFile(sFile);
geo.generate2DFile(sDir + "oil-2d.geo");
geo.generateWells(sDir + "oil-2d-wells.txt");
}
void nmSubWndGrid::genGrid(bool is3D)

@ -43,6 +43,7 @@
#include "nmWxGridVTKContainerWidget.h"
#include "nmWxSelectWellsDlg.h"
#include "nmWxSelectWellsWidget.h"
#include "nmCalculationSolver.h"
#include "nmObjPointWell.h"
@ -932,7 +933,19 @@ void nmSubWndMain::generationMesh()
void nmSubWndMain::solveAndAnalyze()
{
// vtk文件
QString sFileGrid = ZxBaseUtil::getDirOf(s_Dir_Temp, "Nm/PreProcessing") + "mesh.vtk";
// 油藏参数文件
QString sFileReservoirParamters = ZxBaseUtil::getDirOf(s_Dir_Temp, "Nm/PreProcessing") + "par.txt";
// 井数据文件
QString sFileWell = ZxBaseUtil::getDirOf(s_Dir_Temp, "Nm/PreProcessing") + "wells.txt";
// dll路径
QString dllDir = ZxBaseUtil::getDirOf(s_Dir_Temp, "Nm/Solver");
// 后处理文件路径
QString postDir = ZxBaseUtil::getDirOf(s_Dir_Temp, "Nm/PostProcessing");
// 调用求解器
nmCalculationSolver solver;
solver.execSolve(sFileGrid, sFileReservoirParamters, sFileWell, dllDir, postDir);
}
void nmSubWndMain::triggerToolBarAction(int index)

@ -0,0 +1,18 @@
QT -= gui
TARGET = nmCalculation
TEMPLATE = lib
DEFINES += NM_CALCULATION_LIBRARY
include(../../setting.pri)
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += $${wtSrc}/nmNum/nmCalculation/*.cpp
HEADERS += $${wtInclude}/nmNum/nmCalculation/*.h
INCLUDEPATH += $${wtInclude}/nmNum/nmCalculation \
$${geoHome}/3rd/SinglePhaseSolver/include

@ -9,5 +9,6 @@ addSubdirs(nmData)
addSubdirs(nmPlot)
addSubdirs(nmSubWnd)
addSubdirs(nmSubWxs)
addSubdirs(nmCalculation)
fixDepends()

@ -67,6 +67,9 @@ INCLUDEPATH += $${wtInclude}/nmNum/nmXml
INCLUDEPATH += $${wtInclude}/nmNum/nmSubWxs
INCLUDEPATH += $${wtInclude}/nmNum/nmSubWnd
INCLUDEPATH += $${wtInclude}/nmNum/nmCalculation
INCLUDEPATH += $${geoHome}/3rd/SinglePhaseSolver/include
SOURCES += $${wtSrc}/nmNum/nmSubWnd/*.cpp
HEADERS += $${wtInclude}/nmNum/nmSubWnd/*.h
@ -85,5 +88,5 @@ LIBS += -lmProjectManager -lmSubWnd -lmToolPvt
LIBS += -lnmData -lnmXml -lnmPlot -lnmSubWxs
LIBS += -lmGuiJob
LIBS += -liAlgMath
LIBS += -liAlgMath -lnmCalculation

Loading…
Cancel
Save