diff --git a/.gitignore b/.gitignore
index 549e00a2..0c0a974e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,4 @@ build/
### VS Code ###
.vscode/
+*.log
diff --git a/pom.xml b/pom.xml
index 8df0202c..0d703482 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,7 +44,32 @@
test
+
+ commons-collections
+ commons-collections
+ 3.2.2
+
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
+
+ io.vavr
+ vavr
+ 0.10.4
+
+
+ org.apache.commons
+ commons-math3
+ 3.6.1
+
+
+ commons-io
+ commons-io
+ 2.11.0
+
com.google.ortools
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalBlendRequest.java b/src/main/java/cn/lihongjie/coal/dto/CoalBlendRequest.java
index a8d4e861..083a7e66 100644
--- a/src/main/java/cn/lihongjie/coal/dto/CoalBlendRequest.java
+++ b/src/main/java/cn/lihongjie/coal/dto/CoalBlendRequest.java
@@ -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 coals;
- private CoalConstraints constraints;
+ private List coals;
+
+
+ private List constraints;
+
+ private Map constraintMap;
/**
- * 参数优先级
+ * 1 高精度
+ * 2 低精度(用于铲车配煤 类似 1:3 整比例)
*/
- private List priority;
+ private Integer type = 1;
+ /**
+ * 低精度配比之和最大值
+ */
+ private int percent2Sum = 10;
+
+
+ public Map 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;
+
+
}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalBlendResult.java b/src/main/java/cn/lihongjie/coal/dto/CoalBlendResult.java
new file mode 100644
index 00000000..a40c5eef
--- /dev/null
+++ b/src/main/java/cn/lihongjie/coal/dto/CoalBlendResult.java
@@ -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 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;
+
+ }
+
+
+}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalConstraint.java b/src/main/java/cn/lihongjie/coal/dto/CoalConstraint.java
new file mode 100644
index 00000000..ae312ed6
--- /dev/null
+++ b/src/main/java/cn/lihongjie/coal/dto/CoalConstraint.java
@@ -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;
+
+}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalConstraints.java b/src/main/java/cn/lihongjie/coal/dto/CoalConstraints.java
deleted file mode 100644
index 617e3f01..00000000
--- a/src/main/java/cn/lihongjie/coal/dto/CoalConstraints.java
+++ /dev/null
@@ -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;
-
-
-
-
-}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalInfo.java b/src/main/java/cn/lihongjie/coal/dto/CoalInfo.java
new file mode 100644
index 00000000..cc393d3e
--- /dev/null
+++ b/src/main/java/cn/lihongjie/coal/dto/CoalInfo.java
@@ -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 percents;
+
+ private Integer min = 1;
+ private Integer max = 99;
+
+
+ private List parameters;
+
+
+ private Map parameterMap;
+
+
+ public Map 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);
+
+
+ }
+}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalParameter.java b/src/main/java/cn/lihongjie/coal/dto/CoalParameter.java
new file mode 100644
index 00000000..48f66a07
--- /dev/null
+++ b/src/main/java/cn/lihongjie/coal/dto/CoalParameter.java
@@ -0,0 +1,12 @@
+package cn.lihongjie.coal.dto;
+
+import lombok.Data;
+
+@Data
+public class CoalParameter {
+
+ private String code;
+
+ private Double value;
+
+}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalParameters.java b/src/main/java/cn/lihongjie/coal/dto/CoalParameters.java
deleted file mode 100644
index c088b524..00000000
--- a/src/main/java/cn/lihongjie/coal/dto/CoalParameters.java
+++ /dev/null
@@ -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;
-
-
-
-
-
-}
diff --git a/src/main/java/cn/lihongjie/coal/dto/CoalPercent.java b/src/main/java/cn/lihongjie/coal/dto/CoalPercent.java
new file mode 100644
index 00000000..d7a8bc41
--- /dev/null
+++ b/src/main/java/cn/lihongjie/coal/dto/CoalPercent.java
@@ -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;
+}
diff --git a/src/main/java/cn/lihongjie/coal/service/CoalService.java b/src/main/java/cn/lihongjie/coal/service/CoalService.java
index 37809493..8aced3e5 100644
--- a/src/main/java/cn/lihongjie/coal/service/CoalService.java
+++ b/src/main/java/cn/lihongjie/coal/service/CoalService.java
@@ -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 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 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 list = request.getConstraints()
+ .stream()
+ .filter(x -> x.getPriority() != null)
+ .sorted(Comparator.comparing(x -> x.getPriority()))
+ .collect(Collectors.toList());
+
+
+ Comparator c = Comparator.comparing(x -> 1);
+ if (!list.isEmpty()) {
+
+
+ for (CoalConstraint constraint : list) {
+
+ if (constraint.getOrder() == null || constraint.getOrder() == 1) {
+ c =
+ c.thenComparing(Comparator.comparing(x -> x.getParamVal(constraint.getCode())));
+ } else {
+
+ c =
+ c.thenComparing(Comparator.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 vars) {
+
+
+ if (request.getType() == 2) {
+
+ IntVar gcd = model.newIntVar(2, request.getPercent2Sum(), "gcd");
+
+ List 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));
+ }
+
+ }
}
diff --git a/src/test/java/cn/lihongjie/coal/service/CoalServiceTest.java b/src/test/java/cn/lihongjie/coal/service/CoalServiceTest.java
new file mode 100644
index 00000000..e94c3e67
--- /dev/null
+++ b/src/test/java/cn/lihongjie/coal/service/CoalServiceTest.java
@@ -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());
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/cn/lihongjie/coal/service/SimpleSatProgram.java b/src/test/java/cn/lihongjie/coal/service/SimpleSatProgram.java
new file mode 100644
index 00000000..9d954f67
--- /dev/null
+++ b/src/test/java/cn/lihongjie/coal/service/SimpleSatProgram.java
@@ -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() {}
+}
\ No newline at end of file
diff --git a/src/test/resources/request1.json b/src/test/resources/request1.json
new file mode 100644
index 00000000..0edf0a09
--- /dev/null
+++ b/src/test/resources/request1.json
@@ -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
+ }
+ ]
+ }]
+}
\ No newline at end of file