This commit is contained in:
2023-07-21 17:59:56 +08:00
parent a65cb74daa
commit cfab5f1321
14 changed files with 609 additions and 140 deletions

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ build/
### VS Code ###
.vscode/
*.log

25
pom.xml
View File

@@ -44,7 +44,32 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.ortools/ortools-java -->
<dependency>
<groupId>com.google.ortools</groupId>

View File

@@ -3,15 +3,47 @@ package cn.lihongjie.coal.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
public class CoalBlendRequest {
private List<CoalParameters> coals;
private CoalConstraints constraints;
private List<CoalInfo> coals;
private List<CoalConstraint> constraints;
private Map<String, CoalConstraint> constraintMap;
/**
* 参数优先级
* 1 高精度
* 2 低精度(用于铲车配煤 类似 1:3 整比例)
*/
private List<String> priority;
private Integer type = 1;
/**
* 低精度配比之和最大值
*/
private int percent2Sum = 10;
public Map<String, CoalConstraint> getConstraintMap() {
if (constraintMap == null) {
constraintMap = constraints.stream().collect(Collectors.toMap(e -> e.getCode(), e -> e));
}
return constraintMap;
}
/**
* 结果个数
*/
private Long count = 10L;
/**
* 最长等待时间
*/
private Long maxTime = 10L;
}

View File

@@ -0,0 +1,29 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
import java.util.List;
import java.util.stream.Collectors;
@Data
public class CoalBlendResult {
private long numBranches = 0;
private long numConflicts = 0;
private long wallTime = 0;
private long userTime = 0;
private List<CoalInfo> coals;
private int totalCount;
public String tableString() {
String headerLine = String.format("totalCount %s numBranches %s numConflicts %s wallTime %s userTime %s \n",
totalCount, numBranches,
numConflicts, wallTime, userTime);
String collect = coals.stream().map(x -> x.rowString()).collect(Collectors.joining("\n"));
return headerLine + collect;
}
}

View File

@@ -0,0 +1,15 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
@Data
public class CoalConstraint{
private String code;
private Double min;
private Double max;
private Integer priority;
private Integer order;
}

View File

@@ -1,52 +0,0 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
@Data
public class CoalConstraints {
private String param1Min;
private String param1Max;
private String param2Min;
private String param2Max;
private String param3Min;
private String param3Max;
private String param4Min;
private String param4Max;
private String param5Min;
private String param5Max;
private String param6Min;
private String param6Max;
private String param7Min;
private String param7Max;
private String param8Min;
private String param8Max;
private String param9Min;
private String param9Max;
private String param10Min;
private String param10Max;
private String param11Min;
private String param11Max;
private String param12Min;
private String param12Max;
private String param13Min;
private String param13Max;
private String param14Min;
private String param14Max;
private String param15Min;
private String param15Max;
private String param16Min;
private String param16Max;
private String param17Min;
private String param17Max;
private String param18Min;
private String param18Max;
private String param19Min;
private String param19Max;
private String param20Min;
private String param20Max;
}

View File

@@ -0,0 +1,66 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
public class CoalInfo {
private String id;
private String name;
/**
* 配煤比例明细
*/
private List<CoalPercent> percents;
private Integer min = 1;
private Integer max = 99;
private List<CoalParameter> parameters;
private Map<String, CoalParameter> parameterMap;
public Map<String, CoalParameter> getParameterMap() {
if (parameterMap == null) {
parameterMap = parameters.stream().collect(Collectors.toMap(e -> e.getCode(), e -> e));
}
return parameterMap;
}
public Double getParamVal(String code) {
CoalParameter coalParameter = getParameterMap().get(code);
return coalParameter == null ? null : coalParameter.getValue();
}
public String rowString() {
String percentString =
percents.stream().map(x -> String.format("%5s %-3s(%1s)", x.getName(), x.getPercent(),
x.getPercent2())).collect(Collectors.joining("\t"));
String paramString =
parameters.stream().map(x -> String.format("%5s:%-6.2f", x.getCode(), x.getValue())).collect(Collectors.joining(
"\t"));
return String.format("%s\t%20s\t%s", name, percentString, paramString);
}
}

View File

@@ -0,0 +1,12 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
@Data
public class CoalParameter {
private String code;
private Double value;
}

View File

@@ -1,76 +0,0 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
@Data
public class CoalParameters {
private String id;
private String name;
private Boolean optional = false;
/**
* 灰分Ash Content煤中的无燃料部分表示煤中的矿物质含量。
*/
private String param1;
/**
* 挥发分Volatile Matter煤中挥发性物质的含量包括水分、气体和其他易挥发物。
*/
private String param2;
/**
* 硫分Sulfur Content煤中硫的含量因为燃烧高硫煤会产生有害的气体和环境污染物。
*/
private String param3;
/**
* 内水分Moisture Content煤中的水分含量对于煤的储存、运输和燃烧特性有影响。
*/
private String param4;
/**
* 外水分Moisture Content煤中的水分含量对于煤的储存、运输和燃烧特性有影响。
*/
private String param5;
/**
* 热值Calorific Value煤的能量含量也称为热值或发热量是评估煤的燃烧性能和能源价值的重要指标。
*/
private String param6;
/**
* 粒度分析Particle Size Distribution测定煤中颗粒的大小和分布这对于煤的处理、燃烧和利用具有重要意义。
*/
private String param7;
/**
* 可磨性指数Grindability Index衡量煤的磨煤性能即煤在磨煤设备中的易磨性。
*/
private String param8;
/**
* 可溶性物质Soluble Matter煤中可溶于特定溶剂的物质的含量对煤的加工和化学利用具有重要影响。
*/
private String param9;
/**
* 阻燃指数Flame Retardant Index测定煤在燃烧过程中的阻燃性能用于评估煤的安全性和环境影响。
*/
private String param10;
/**
* 硫酸盐Sulfates测定煤中硫酸盐的含量这对于煤的脱硫和环境影响具有重要意义。
*/
private String param11;
/**
* 可燃性气体Combustible Gas测定煤中可燃性气体如甲烷的含量这对于煤矿安全和煤层气开发有重要意义。
*/
private String param12;
/**
* 成本
*/
private String param13;
}

View File

@@ -0,0 +1,17 @@
package cn.lihongjie.coal.dto;
import lombok.Data;
@Data
public class CoalPercent {
private String id;
private String name;
private Long percent;
/**
* gcd之后的比例
*/
private Long percent2;
}

View File

@@ -1,12 +1,23 @@
package cn.lihongjie.coal.service;
import cn.lihongjie.coal.dto.CoalBlendRequest;
import cn.lihongjie.coal.dto.CoalParameters;
import ch.qos.logback.core.BasicStatusManager;
import cn.lihongjie.coal.dto.*;
import com.google.ortools.Loader;
import com.google.ortools.sat.CpModel;
import com.google.ortools.sat.IntVar;
import com.google.ortools.sat.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.util.ArithmeticUtils;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class CoalService {
@@ -16,17 +27,251 @@ public class CoalService {
*
* @return
*/
public Object blend(CoalBlendRequest request) {
public CoalBlendResult blend(CoalBlendRequest request) {
Loader.loadNativeLibraries();
CoalBlendResult result = new CoalBlendResult();
result.setCoals(new ArrayList<>());
CpModel model = new CpModel();
int index = 0;
for (CoalParameters coal : request.getCoals()) {
IntVar x = model.newIntVar( coal.getOptional() ? 0 : 1, 100 , "x" + index);
List<IntVar> vars = new ArrayList<>();
for (CoalInfo coal : request.getCoals()) {
IntVar x = model.newIntVar(Math.max(coal.getMin(), 0), Math.min(coal.getMax(), 100), "x" + index++);
vars.add(x);
}
addConstrains(request, model, vars);
// 各种煤比例之和为 100
model.addEquality(LinearExpr.sum(vars.toArray(new LinearArgument[0])), 100);
CpSolver cpSolver = new CpSolver();
cpSolver.setLogCallback(x -> log.info(x));
SatParameters.Builder parameters = cpSolver.getParameters();
parameters.setEnumerateAllSolutions(true);
parameters.setMaxTimeInSeconds(request.getMaxTime());
parameters.setLogSearchProgress(true);
parameters.setSearchBranching(SatParameters.SearchBranching.FIXED_SEARCH);
parameters.setFillAdditionalSolutionsInResponse(true);
// 记录结果
CpSolverStatus solverStatus = cpSolver.solve(model, new CpSolverSolutionCallback() {
@Override
public void onSolutionCallback() {
CoalInfo solution = new CoalInfo();
List<Long> gcdVals = new ArrayList<>();
solution.setPercents(new ArrayList<>());
solution.setParameters(new ArrayList<>());
for (int i = 0; i < vars.size(); i++) {
long percent = this.value(vars.get(i));
CoalInfo coal = request.getCoals().get(i);
// 累加
try {
acc(solution, percent, coal);
} catch (Exception e) {
e.printStackTrace();
}
CoalPercent e = new CoalPercent();
e.setId(coal.getId());
e.setName(coal.getName());
e.setPercent(percent);
if (CollectionUtils.isNotEmpty(gcdVals)) {
e.setPercent2(gcdVals.get(i));
}
solution.getPercents().add(e);
}
// 四舍五入
round(solution);
result.getCoals().add(solution);
}
});
int totalCount = result.getCoals().size();
sortAndSelect(request, result);
result.setTotalCount(totalCount);
result.setNumBranches(cpSolver.numBranches());
result.setNumConflicts(cpSolver.numConflicts());
result.setUserTime((long) cpSolver.userTime());
result.setWallTime((long) cpSolver.wallTime());
return result;
}
private void sortAndSelect(CoalBlendRequest request, CoalBlendResult result) {
List<CoalConstraint> list = request.getConstraints()
.stream()
.filter(x -> x.getPriority() != null)
.sorted(Comparator.comparing(x -> x.getPriority()))
.collect(Collectors.toList());
Comparator<CoalInfo> c = Comparator.comparing(x -> 1);
if (!list.isEmpty()) {
for (CoalConstraint constraint : list) {
if (constraint.getOrder() == null || constraint.getOrder() == 1) {
c =
c.thenComparing(Comparator.<CoalInfo, Double>comparing(x -> x.getParamVal(constraint.getCode())));
} else {
c =
c.thenComparing(Comparator.<CoalInfo, Double>comparing(x -> x.getParamVal(constraint.getCode())).reversed());
}
}
}
result.setCoals(result.getCoals().stream().sorted(c).limit(request.getCount()).collect(Collectors.toList()));
}
private void round(CoalInfo solution) {
solution.getParameters().forEach(x -> x.setValue(x.getValue() == null ? x.getValue() :
BigDecimal.valueOf(x.getValue()).setScale(2, RoundingMode.HALF_UP).doubleValue()));
}
private void acc(CoalInfo solution, long percent, CoalInfo coal) {
for (CoalParameter parameter : coal.getParameters()) {
boolean found = false;
for (CoalParameter solutionParameter : solution.getParameters()) {
if (StringUtils.equalsIgnoreCase(parameter.getCode(), solutionParameter.getCode())) {
solutionParameter.setValue(solutionParameter.getValue() + (parameter.getValue() * percent / 100.0));
found = true;
break;
}
}
if (!found) {
CoalParameter e = new CoalParameter();
e.setCode(parameter.getCode());
e.setValue(parameter.getValue() * percent / 100.0);
solution.getParameters().add(e);
}
}
return null;
}
private void addConstrains(CoalBlendRequest request, CpModel model, List<IntVar> vars) {
if (request.getType() == 2) {
IntVar gcd = model.newIntVar(2, request.getPercent2Sum(), "gcd");
List<IntVar> varGcdList = new ArrayList<>();
for (IntVar var : vars) {
model.addModuloEquality(model.newConstant(0), var, gcd);
// 定义一个变量,表示 percent / gcd
IntVar varGcd = model.newIntVar(1, 100, var.getName() + "_gcd");
// 约束这个变量
model.addDivisionEquality(varGcd, var, gcd);
varGcdList.add(varGcd);
}
// 约束 sum(percent / gcd) < request.getPercent2Sum()
model.addLessOrEqual(LinearExpr.sum(varGcdList.toArray(new IntVar[0])), request.getPercent2Sum());
}
for (CoalConstraint constrain : request.getConstraints()) {
if (StringUtils.isBlank(constrain.getCode())) {
continue;
}
if (constrain.getMin() == null && constrain.getMax() == null) {
continue;
}
long[] paramsOfEachCoal = new long[request.getCoals().size()];
int index = 0;
for (CoalInfo coal : request.getCoals()) {
boolean found = false;
for (CoalParameter parameter : coal.getParameters()) {
if (StringUtils.equalsIgnoreCase(parameter.getCode(), constrain.getCode())) {
paramsOfEachCoal[index++] = (long) (parameter.getValue() * 100);
found = true;
break;
}
}
if (!found) {
throw new RuntimeException(String.format("煤 %s 没有找到指标 %s, 但是存在条件 %s <= %s <= %s", coal.getName(),
constrain.getCode(), constrain.getMin(), constrain.getCode(), constrain.getMax()));
}
}
if (constrain.getMin() != null) {
model.addGreaterOrEqual(LinearExpr.weightedSum(vars.toArray(new LinearArgument[0]), paramsOfEachCoal),
(long) ((constrain.getMin()) * 10000));
}
if (constrain.getMax() != null) {
model.addLessOrEqual(LinearExpr.weightedSum(vars.toArray(new LinearArgument[0]), paramsOfEachCoal),
(long) ((constrain.getMax()) * 10000));
}
}
}

View File

@@ -0,0 +1,34 @@
package cn.lihongjie.coal.service;
import cn.lihongjie.coal.dto.CoalBlendRequest;
import cn.lihongjie.coal.dto.CoalBlendResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
class CoalServiceTest {
@SneakyThrows
@Test
void testBlend() {
CoalService coalService = new CoalService();
InputStream stream = this.getClass().getClassLoader().getResourceAsStream(
"request1.json");
CoalBlendRequest coalBlendRequest = new ObjectMapper().readValue(stream, CoalBlendRequest.class);
for (int i = 0; i < 1; i++) {
CoalBlendResult result = coalService.blend(coalBlendRequest);
System.out.println(result.tableString());
}
}
}

View File

@@ -0,0 +1,39 @@
package cn.lihongjie.coal.service;
import com.google.ortools.Loader;
import com.google.ortools.sat.CpModel;
import com.google.ortools.sat.CpSolver;
import com.google.ortools.sat.CpSolverStatus;
import com.google.ortools.sat.IntVar;
/** Minimal CP-SAT example to showcase calling the solver. */
public final class SimpleSatProgram {
public static void main(String[] args) throws Exception {
Loader.loadNativeLibraries();
// Create the model.
CpModel model = new CpModel();
// Create the variables.
int numVals = 3;
IntVar x = model.newIntVar(0, numVals - 1, "x");
IntVar y = model.newIntVar(0, numVals - 1, "y");
IntVar z = model.newIntVar(0, numVals - 1, "z");
// Create the constraints.
model.addDifferent(x, y);
// Create a solver and solve the model.
CpSolver solver = new CpSolver();
CpSolverStatus status = solver.solve(model);
if (status == CpSolverStatus.OPTIMAL || status == CpSolverStatus.FEASIBLE) {
System.out.println("x = " + solver.value(x));
System.out.println("y = " + solver.value(y));
System.out.println("z = " + solver.value(z));
} else {
System.out.println("No solution found.");
}
}
private SimpleSatProgram() {}
}

View File

@@ -0,0 +1,82 @@
{
"type": 1,
"maxTime": 100,
"constraints": [
{
"code": "param1",
"min": 1.5,
"max": 1.9,
"priority": 1,
"order": -1
},
{
"code": "param2",
"min": 2,
"max": 2.8,
"priority": 3,
"order": -1
},
{
"code": "param3",
"min": 3,
"max": 5,
"priority": 4,
"order": -1
}
],
"coals": [
{
"id": 1,
"name": "煤1",
"parameters": [
{
"code": "param1",
"value": 1
},
{
"code": "param2",
"value": 2
},
{
"code": "param3",
"value": 3
}
]
},
{
"id": 2,
"name": "煤2",
"parameters": [
{
"code": "param1",
"value": 2
},
{
"code": "param2",
"value": 3
},
{
"code": "param3",
"value": 5
}
]
},
{
"id": 3,
"name": "煤3",
"parameters": [
{
"code": "param1",
"value": 2
},
{
"code": "param2",
"value": 3
},
{
"code": "param3",
"value": 5
}
]
}]
}