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