diff --git a/build.gradle b/build.gradle index bd12fee..9115dfe 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,8 @@ dependencies { implementation group: 'org.springframework.boot', name: 'spring-boot-starter-jetty' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-test' + // web client + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux' implementation group: 'org.slf4j', name: 'slf4j-api' implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect' implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity6' diff --git a/src/main/java/ru/ulstu/http/HttpService.java b/src/main/java/ru/ulstu/http/HttpService.java new file mode 100644 index 0000000..282f5bd --- /dev/null +++ b/src/main/java/ru/ulstu/http/HttpService.java @@ -0,0 +1,47 @@ +package ru.ulstu.http; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import ru.ulstu.method.fuzzy.FuzzyRuleDataDto; +import ru.ulstu.method.fuzzy.OutputValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +public class HttpService { + private final WebClient client = getWebClient(); + private final static String USER_NAME = "admin"; + private final static String PASSWORD = "admin"; + + public WebClient getWebClient() { + return WebClient.builder() + .defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + + Base64.getEncoder().encodeToString((USER_NAME + ":" + PASSWORD).getBytes())) + .build(); + } + + public List post(String url, FuzzyRuleDataDto requestBody) { + List result = new ArrayList<>(); + + try { + OutputValue[] res = client + .post() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(requestBody), FuzzyRuleDataDto.class) + .retrieve() + .bodyToMono(OutputValue[].class) + .block(); + if (res != null && res.length > 0) { + result = Arrays.stream(res).toList(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/src/main/java/ru/ulstu/method/fuzzy/FuzzyRuleDataDto.java b/src/main/java/ru/ulstu/method/fuzzy/FuzzyRuleDataDto.java new file mode 100644 index 0000000..37eb0ea --- /dev/null +++ b/src/main/java/ru/ulstu/method/fuzzy/FuzzyRuleDataDto.java @@ -0,0 +1,33 @@ +package ru.ulstu.method.fuzzy; + +public class FuzzyRuleDataDto { + private String[] fuzzyTerms; + private int window = 3; + private int horizon = 1; + + public FuzzyRuleDataDto(String[] fuzzyTimeSeries, int window, int horizon) { + this.fuzzyTerms = fuzzyTimeSeries; + this.window = window; + this.horizon = horizon; + } + + public String[] getFuzzyTerms() { + return fuzzyTerms; + } + + public void setFuzzyTerms(String[] fuzzyTerms) { + this.fuzzyTerms = fuzzyTerms; + } + + public int getWindow() { + return window; + } + + public void setWindow(int window) { + this.window = window; + } + + public int getHorizon() { + return horizon; + } +} diff --git a/src/main/java/ru/ulstu/method/fuzzy/OutputValue.java b/src/main/java/ru/ulstu/method/fuzzy/OutputValue.java new file mode 100644 index 0000000..ea40027 --- /dev/null +++ b/src/main/java/ru/ulstu/method/fuzzy/OutputValue.java @@ -0,0 +1,37 @@ +package ru.ulstu.method.fuzzy; + +public class OutputValue { + private String variable; + private String fuzzyTerm; + private Double degree; + + public OutputValue(String variable, String fuzzyTerm, Double degree) { + this.variable = variable; + this.fuzzyTerm = fuzzyTerm; + this.degree = degree; + } + + public String getFuzzyTerm() { + return fuzzyTerm; + } + + public void setFuzzyTerm(String fuzzyTerm) { + this.fuzzyTerm = fuzzyTerm; + } + + public Double getDegree() { + return degree; + } + + public void setDegree(Double degree) { + this.degree = degree; + } + + public String getVariable() { + return variable; + } + + public void setVariable(String variable) { + this.variable = variable; + } +} diff --git a/src/main/java/ru/ulstu/method/fuzzy/PlainFuzzy.java b/src/main/java/ru/ulstu/method/fuzzy/PlainFuzzy.java new file mode 100644 index 0000000..c0c5b9b --- /dev/null +++ b/src/main/java/ru/ulstu/method/fuzzy/PlainFuzzy.java @@ -0,0 +1,99 @@ +package ru.ulstu.method.fuzzy; + +import org.springframework.stereotype.Component; +import ru.ulstu.datamodel.Model; +import ru.ulstu.datamodel.exception.ModelingException; +import ru.ulstu.datamodel.ts.TimeSeries; +import ru.ulstu.datamodel.ts.TimeSeriesValue; +import ru.ulstu.http.HttpService; +import ru.ulstu.method.Method; +import ru.ulstu.method.MethodParamValue; +import ru.ulstu.method.MethodParameter; + +import java.util.ArrayList; +import java.util.DoubleSummaryStatistics; +import java.util.List; + +@Component +public class PlainFuzzy extends Method { + private final HttpService httpService = new HttpService(); + + @Override + public List getAvailableParameters() { + return PlainFuzzyModel.getAvailableParameters(); + } + + @Override + protected Model getModelOfValidTimeSeries(TimeSeries timeSeries, List parameters) { + PlainFuzzyModel model = new PlainFuzzyModel(timeSeries, parameters); + List fuzzySets = generateFuzzySets(timeSeries, model.getNumberOfFuzzyTerms().getIntValue()); + model.setFuzzySets(fuzzySets); + for (TimeSeriesValue tsVal : timeSeries.getValues()) { + model.getTimeSeriesModel().addValue(new TimeSeriesValue( + tsVal.getDate(), + getMaxMembership(fuzzySets, tsVal).getTop())); + model.getFuzzyTimeSeries().add(getMaxMembership(fuzzySets, tsVal)); + } + return model; + } + + private List generateFuzzySets(TimeSeries timeSeries, Integer numberOfFuzzyTerms) { + // Universum + DoubleSummaryStatistics stat = timeSeries.getValues() + .stream() + .mapToDouble(TimeSeriesValue::getValue) + .summaryStatistics(); + double min = stat.getMin(); + double max = stat.getMax(); + + // Generate fuzzy sets + List fuzzySets = new ArrayList<>(); + double delta = ((max - min) / (numberOfFuzzyTerms - 1)); + + for (int i = 0; i < numberOfFuzzyTerms; i++) { + fuzzySets.add(new Triangle(min + i * delta - delta, + min + i * delta, + min + i * delta + delta, i)); + } + return fuzzySets; + } + + private Triangle getMaxMembership(List fuzzySets, TimeSeriesValue tsVal) { + Triangle maxTriangle = fuzzySets.get(0); + double membersip = 0; + for (Triangle triangle : fuzzySets) { + if (membersip < triangle.getValueAtPoint(tsVal.getValue())) { + maxTriangle = triangle; + membersip = triangle.getValueAtPoint(tsVal.getValue()); + } + } + return maxTriangle; + } + + @Override + protected TimeSeries getForecastWithValidParams(Model model, TimeSeries forecast) throws ModelingException { + PlainFuzzyModel pfm = ((PlainFuzzyModel) model); + List fuzzyTimeSeries = pfm.getFuzzyTimeSeries().stream().map(Triangle::getLabel).toList(); + List result = httpService.post("http://plans.athene.tech/inferenceRest/get-inference-by-generated-rules", + new FuzzyRuleDataDto(fuzzyTimeSeries.toArray(String[]::new), 2, forecast.getLength())); + List forecastValues = result.stream().map(r -> pfm.getTopValueByLabel(r.getFuzzyTerm())).toList(); + List values = forecast.getValues(); + for (int i = 0; i < values.size(); i++) { + if (forecastValues.isEmpty()) { + values.get(i).setValue(pfm.getTimeSeriesModel().getValues().getLast().getValue()); + } else { + if (forecastValues.size() > i) { + values.get(i).setValue(forecastValues.get(i)); + } else { + values.get(i).setValue(forecastValues.getLast()); + } + } + } + return forecast; + } + + @Override + public String getName() { + return "Нечеткая модель"; + } +} diff --git a/src/main/java/ru/ulstu/method/fuzzy/PlainFuzzyModel.java b/src/main/java/ru/ulstu/method/fuzzy/PlainFuzzyModel.java new file mode 100644 index 0000000..97bf6eb --- /dev/null +++ b/src/main/java/ru/ulstu/method/fuzzy/PlainFuzzyModel.java @@ -0,0 +1,55 @@ +package ru.ulstu.method.fuzzy; + +import ru.ulstu.datamodel.Model; +import ru.ulstu.datamodel.ts.TimeSeries; +import ru.ulstu.method.MethodParamValue; +import ru.ulstu.method.MethodParameter; +import ru.ulstu.method.fuzzy.parameter.NumberOfFuzzyTerms; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PlainFuzzyModel extends Model { + private final MethodParamValue numberOfFuzzyTerms = new MethodParamValue(NumberOfFuzzyTerms.getInstance(), 2); + private final List fuzzyTimeSeries = new ArrayList<>(); + private List fuzzySets = new ArrayList<>(); + + protected PlainFuzzyModel(TimeSeries ts, List parameters) { + super(ts); + for (MethodParamValue parameter : parameters) { + if (parameter.getParameter() instanceof NumberOfFuzzyTerms) { + numberOfFuzzyTerms.setValue(parameter.getValue()); + } + } + } + + public static List getAvailableParameters() { + return Collections.singletonList(NumberOfFuzzyTerms.getInstance()); + } + + public MethodParamValue getNumberOfFuzzyTerms() { + return numberOfFuzzyTerms; + } + + public List getFuzzyTimeSeries() { + return fuzzyTimeSeries; + } + + public void setFuzzySets(List fuzzySets) { + this.fuzzySets = fuzzySets; + } + + public List getFuzzySets() { + return fuzzySets; + } + + public double getTopValueByLabel(String label) { + return fuzzySets + .stream() + .filter(fs -> fs.getLabel().equals(label)) + .findAny() + .orElseThrow(() -> new RuntimeException("Неизвестная нечеткая метка")) + .getTop(); + } +} diff --git a/src/main/java/ru/ulstu/method/fuzzy/Triangle.java b/src/main/java/ru/ulstu/method/fuzzy/Triangle.java new file mode 100644 index 0000000..5ebf550 --- /dev/null +++ b/src/main/java/ru/ulstu/method/fuzzy/Triangle.java @@ -0,0 +1,58 @@ +package ru.ulstu.method.fuzzy; + +public class Triangle { + private final static String FUZZY_LABEL_TEMPLATE = "X%s"; + private String label; + private double start; // левая граница треугольника + private double end; // правая граница треугольника + private double top; // вершина треугольника + + public double getStart() { + return start; + } + + public Triangle(double start, double top, double end, int number) { + this.start = start; + this.top = top; + this.end = end; + this.label = String.format(FUZZY_LABEL_TEMPLATE, number); + } + + public void setStart(int start) { + this.start = start; + } + + + public double getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + + public double getTop() { + return top; + } + + public void setTop(int top) { + this.top = top; + } + + public double getValueAtPoint(double crispValue) { + if (crispValue == this.getTop()) { + return 1; + } else if ((crispValue >= this.getEnd()) || (crispValue <= this.getStart())) { + return 0; + } else if (crispValue < this.getTop()) { + return (crispValue - this.getStart()) / (this.getTop() - this.getStart()); + } else if (crispValue > this.getTop()) { + return -(crispValue - this.getEnd()) / (this.getEnd() - this.getTop()); + } + return 0; + } + + public String getLabel() { + return label; + } +} diff --git a/src/main/java/ru/ulstu/method/fuzzy/parameter/NumberOfFuzzyTerms.java b/src/main/java/ru/ulstu/method/fuzzy/parameter/NumberOfFuzzyTerms.java new file mode 100644 index 0000000..ff22a83 --- /dev/null +++ b/src/main/java/ru/ulstu/method/fuzzy/parameter/NumberOfFuzzyTerms.java @@ -0,0 +1,32 @@ +package ru.ulstu.method.fuzzy.parameter; + +import ru.ulstu.datamodel.ts.TimeSeries; +import ru.ulstu.method.MethodParameter; + +import java.util.ArrayList; +import java.util.List; + +public class NumberOfFuzzyTerms extends MethodParameter { + private final static int MIN_NUMBER_OF_FUZZY_TERMS = 2; + private final static int MIN_INCREASING_STEP_OF_NUMBER_OF_FUZZY_TERMS = 1; + private final static int MAX_NUMBER_OF_FUZZY_TERMS = 7; + + public NumberOfFuzzyTerms() { + super("Number of fuzzy terms"); + } + + public static NumberOfFuzzyTerms getInstance() { + return new NumberOfFuzzyTerms(); + } + + @Override + public List getAvailableValues(TimeSeries timeSeries) { + List values = new ArrayList<>(); + for (double i = MIN_NUMBER_OF_FUZZY_TERMS; + i <= MAX_NUMBER_OF_FUZZY_TERMS; + i += MIN_INCREASING_STEP_OF_NUMBER_OF_FUZZY_TERMS) { + values.add(i); + } + return values; + } +}