diff --git a/docker-compose.yml b/docker-compose.yml index f4edaae..f576ccc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ x-proxy-build-args: &proxy-build-args HTTP_PROXY: ${HTTP_PROXY_ARG:-} HTTPS_PROXY: ${HTTPS_PROXY_ARG:-} - NO_PROXY: ${NO_PROXY_ARG:-localhost,127.0.0.1,gateway,gateway-dev,engine-minizinc,engine-random-search,engine-many-heuristic,frontend-dev,frontend-prod,nginx,nginx-dev} + NO_PROXY: ${NO_PROXY_ARG:-localhost,127.0.0.1,gateway,gateway-dev,engine-minizinc,engine-random-search,engine-many-heuristic,engine-evolutionary-heuristics,frontend-dev,frontend-prod,nginx,nginx-dev} services: gateway: @@ -24,6 +24,7 @@ services: - ENGINE_MINIZINC_URL=http://engine-minizinc:3000 - ENGINE_RANDOM_SEARCH_URL=http://engine-random-search:8080 - ENGINE_MANY_HEURISTIC_URL=http://engine-many-heuristic:8080 + - ENGINE_EVOLUTIONARY_HEURISTICS_URL=http://engine-evolutionary-heuristics:8080 healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"] interval: 10s @@ -37,6 +38,8 @@ services: condition: service_healthy engine-many-heuristic: condition: service_healthy + engine-evolutionary-heuristics: + condition: service_healthy gateway-dev: build: @@ -60,6 +63,7 @@ services: - ENGINE_MINIZINC_URL=http://engine-minizinc:3000 - ENGINE_RANDOM_SEARCH_URL=http://engine-random-search:8080 - ENGINE_MANY_HEURISTIC_URL=http://engine-many-heuristic:8080 + - ENGINE_EVOLUTIONARY_HEURISTICS_URL=http://engine-evolutionary-heuristics:8080 healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"] interval: 10s @@ -73,6 +77,8 @@ services: condition: service_healthy engine-many-heuristic: condition: service_healthy + engine-evolutionary-heuristics: + condition: service_healthy engine-minizinc: build: @@ -116,6 +122,20 @@ services: retries: 5 start_period: 30s + engine-evolutionary-heuristics: + build: + context: ./engines/evolutionary-heuristics + dockerfile: Dockerfile + args: + <<: *proxy-build-args + profiles: ["dev", "prod"] + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 30s + frontend-dev: build: context: ./frontend diff --git a/engines/evolutionary-heuristics/DESIGN_RATIONALE.md b/engines/evolutionary-heuristics/DESIGN_RATIONALE.md new file mode 100644 index 0000000..ddb6733 --- /dev/null +++ b/engines/evolutionary-heuristics/DESIGN_RATIONALE.md @@ -0,0 +1,522 @@ +# Evolutionary Heuristics Engine: Design Rationale + +## Purpose + +This document records the main architectural and algorithmic decisions made +while implementing the evolutionary heuristic solvers for OpenBinding. It +complements the operational description in `README.md` by explaining why the +current design was selected, which alternatives were considered, and which +trade-offs remain open for experimental validation. + +## Decision 1: Implement an independent engine + +### Decision + +The evolutionary solvers are implemented as a separate service under +`engines/evolutionary-heuristics`. + +### Rationale + +The existing Java heuristic engines target Java 7/8 and contain a legacy domain +model tightly coupled to their solver implementations. Current jMetal versions +require a modern Java runtime and provide APIs that do not fit cleanly into that +codebase without a broad migration. + +Keeping the engine independent provides: + +- Java 21 without changing the runtime of existing engines. +- An explicit integration boundary through the OpenBinding HTTP contract. +- Independent dependency, deployment, and performance tuning. +- A controlled place for experimental algorithms and quality indicators. +- Lower regression risk for the existing random and many-objective engines. + +### Alternatives considered + +- **Extend `many-heuristic`:** rejected because it would combine legacy model + migration with the new optimization work. +- **Implement the engine in Python:** viable, especially with pymoo or + jMetalPy, but inconsistent with the existing heuristic engine ecosystem and + less direct for sharing JVM-based experimental infrastructure. +- **Replace the existing heuristic engines:** rejected because the new engine + is initially experimental and should be evaluated before replacing established + behavior. + +### Consequence + +Some mapping and evaluation concepts are duplicated across services. The +gateway remains responsible for canonical output normalization, while the +engine must preserve equivalent aggregation semantics during optimization. + +## Decision 2: Use Java 21 and jMetal 7.4 + +### Decision + +The engine targets Java 21 and uses jMetal 7.4 for evolutionary algorithms. + +### Rationale + +jMetal is focused on single-, multi-, and many-objective metaheuristics and +provides: + +- NSGA-II and NSGA-III implementations. +- Bounded integer solutions and evolutionary operators. +- Constraint-aware solution comparison. +- Standard experiment and quality-indicator infrastructure. +- A direct path to MOEA/D, RVEA, and other algorithms for future comparison. + +Java 21 is the runtime adopted by jMetal 7.4 and also allows lightweight HTTP +request handling with virtual threads. + +### Alternatives considered + +- **Jenetics:** has a clean API and strong general genetic algorithm support, + but jMetal better matches the research requirement for Pareto-based and + many-objective algorithms. +- **Custom genetic algorithm:** rejected because selection, ranking, diversity, + and reference-point behavior are established algorithmic components that + should not be reimplemented without a specific research reason. +- **Older jMetal release:** rejected to avoid starting on an obsolete Java/API + baseline and to retain current constraint-handling improvements. + +### Consequence + +The engine image is larger than the legacy engines and requires Java 21. This +is accepted because engines are independently containerized. + +## Decision 3: Accept the general OpenBinding instance + +### Decision + +The gateway sends: + +```json +{ + "instance": "", + "options": {} +} +``` + +The engine does not use the legacy random-search DTO. + +### Rationale + +The general model contains the information required to implement the intended +semantics without lossy translation: + +- Objective type and targets. +- Hard versus soft constraints. +- Feature ranges and directions. +- Structured aggregation policies. +- Candidate/provider information. + +Using the general model also keeps the engine independent from implementation +details of the existing Java engines. + +### Consequence + +The gateway plugin is deliberately thin. General schema validation and common +semantic validation occur before routing, while the engine performs defensive +checks required for standalone execution. + +## Decision 4: Represent bindings as bounded integer vectors + +### Decision + +An individual has one integer variable per abstract task. The variable value is +the index of the selected candidate for that task. + +### Rationale + +This representation matches the structure of the binding problem: + +```text +gene i = candidate selected for abstract task i +``` + +It guarantees by construction that: + +- Every individual assigns one candidate to every task. +- Every allele references a candidate in the corresponding task domain. +- Crossover and mutation cannot create an unknown service identifier. +- Decoding is linear in the number of tasks. + +Task order is collected deterministically from the composition tree using +insertion order. Repeated references to the same abstract task share one gene. + +### Alternatives considered + +- **Binary encoding:** rejected because candidate domains have different sizes + and binary operators would generate invalid encodings. +- **Permutation encoding:** rejected because service binding is an assignment + problem, not an ordering problem. +- **Provider-first hierarchical encoding:** potentially useful for dependency + constraints, but more complex and not universally beneficial. + +### Consequence + +The current implementation uses jMetal integer variation operators. A custom +categorical uniform crossover and random-reset mutation may be a better +domain-specific choice and remains an explicit experimental refinement. + +## Decision 5: Evaluate global QoS over the composition tree + +### Decision + +Fitness evaluation decodes the binding and recursively evaluates each target +feature over `TASK`, `SEQ`, `AND`, `XOR`, `LOOP`, and `ELEMENT` nodes. + +### Rationale + +Binding quality is not the sum of independent local rankings. It depends on the +workflow structure and feature-specific aggregation rules. For example: + +- Sequential latency is usually additive. +- Parallel latency is usually the maximum branch latency. +- Reliability and availability are commonly multiplicative. +- XOR branches require probability-weighted aggregation. +- Loop contributions depend on the expected iteration count. + +The evolutionary algorithm must therefore optimize the global composed quality, +not a proxy based only on candidate-level scores. + +### Consequence + +Evaluation is the main computational cost. Future performance work should +prioritize chromosome memoization, incremental evaluation, and parallel +population evaluation before changing algorithmic operators. + +## Decision 6: Keep raw, aggregated, normalized, and optimization values separate + +### Decision + +Candidate feature values are never modified. Evaluation maintains separate +representations for: + +1. Raw candidate values. +2. Globally aggregated values. +3. Normalized quality/loss values. +4. The objective vector consumed by jMetal. + +### Rationale + +Mutating candidate values during scaling makes repeated evaluation difficult to +reason about and can scale constraints inconsistently. Separation provides: + +- Stable input data. +- Traceable output values. +- Consistent constraint evaluation in the original units. +- Independent adjustment of normalization policies. + +### Consequence + +The public response reports original-unit aggregated features, while solution +metadata reports the internal minimization vector. + +## Decision 7: Normalize after global aggregation + +### Decision + +Optimization losses are computed from the globally aggregated value using an +explicit normalization policy when available, otherwise the feature +`valid_range`. + +For minimization: + +```text +loss = clamp((Q - Qmin) / (Qmax - Qmin)) +``` + +For maximization: + +```text +loss = 1 - clamp((Q - Qmin) / (Qmax - Qmin)) +``` + +### Rationale + +Objectives expressed in different units cannot be compared or combined +directly. Normalizing the final aggregate preserves workflow semantics and +prevents candidate-level normalization from changing non-linear aggregation +behavior. + +### Percentage product handling + +Ratio features whose valid maximum is greater than one, such as availability in +`[0, 100]`, are converted to `[0, 1]` before multiplicative composition and +converted back afterwards. This avoids multiplying percentages as if `99` +represented a probability of ninety-nine. + +### Consequence + +Clipping stabilizes optimization but makes all values outside the configured +range equally bad once they cross a bound. Better dynamic or instance-derived +normalization may be evaluated later. + +## Decision 8: Convert every optimization objective to minimization + +### Decision + +The internal jMetal objective vector always represents losses to minimize. + +### Rationale + +A uniform orientation simplifies dominance and algorithm configuration. +Feature direction remains part of the mapping from global QoS to normalized +loss. + +The API-level `objective_value` is kept as a weighted quality score where larger +is better, preserving an intuitive external summary while exposing the internal +vector in metadata. + +### Consequence + +The summary score must not be used to reconstruct Pareto dominance for +`MULTI`/`MANY`; consumers should use the objective vector or aggregated +features. + +## Decision 9: Use NSGA-II for MONO/MULTI and NSGA-III for MANY + +### Decision + +`AUTO` resolves algorithms as follows: + +- `MONO`: NSGA-II with one weighted objective. +- `MULTI`: NSGA-II with one objective per target. +- `MANY`: NSGA-III with one objective per target. + +### Rationale + +NSGA-II is a well-established baseline for two and three objectives. NSGA-III +uses reference points to preserve diversity when Pareto dominance loses +selection pressure with many objectives. + +Using NSGA-II for the first mono-objective implementation reduces the number of +algorithm integration paths. A dedicated generational GA can be introduced +later if experiments show a material benefit. + +### Consequence + +The mono-objective path currently carries some multi-objective infrastructure +overhead. This is acceptable for the initial implementation and should be +measured before specialization. + +## Decision 10: Do not collapse MULTI/MANY into a weighted sum + +### Decision + +Weights are used for mono-objective optimization and for the public summary +score. Multi- and many-objective optimization retain one independent objective +per target. + +### Rationale + +A weighted sum can miss non-convex regions of the Pareto front and returns only +one preference-specific compromise. OpenBinding's `MULTI` and `MANY` models +require a set of trade-off solutions. + +### Consequence + +The engine returns an archive/front rather than one binding for `MULTI` and +`MANY`. `archive_size` bounds response size. + +## Decision 11: Apply feasibility-first handling to hard constraints + +### Decision + +All normalized hard violations are summed and exposed as a jMetal constraint +value. Feasible solutions are preferred over infeasible solutions. + +### Rationale + +Hard constraints define admissibility and should not be traded against QoS +through an arbitrary penalty coefficient. Feasibility-first comparison avoids +a situation where a sufficiently good quality score compensates for an invalid +binding. + +Violations are normalized: + +- Attribute bounds by the corresponding feature range. +- Provider dependencies by the number of involved tasks. + +This prevents constraints with large physical units from dominating the total +violation. + +### Consequence + +If no feasible solution is found, the engine returns the least-violating +solutions instead of failing silently. Their `feasible` metadata and violation +details make this explicit. + +## Decision 12: Treat soft constraints differently by objective mode + +### Decision + +- `MONO`: add `soft_penalty * normalized_soft_violation` to the weighted loss. +- `MULTI`/`MANY`: append total soft violation as an additional objective. + +### Rationale + +Mono-objective optimization requires a scalar ordering, so an explicit penalty +is practical and configurable. In Pareto optimization, adding soft violation as +an objective preserves the trade-off between QoS and preference satisfaction +without hiding it behind one coefficient. + +### Alternatives considered + +- **Lexicographic comparison:** predictable but prevents any QoS/soft-constraint + trade-off. +- **One objective per soft constraint:** maximally expressive but can increase + dimensionality dramatically. +- **Treat soft constraints as hard:** violates their intended semantics. + +### Consequence + +The current multi/many strategy aggregates all soft violations. Per-constraint +objectives may be useful for small numbers of semantically distinct preferences +and should be studied separately. + +## Decision 13: Start without repair operators + +### Decision + +The initial engine relies on evolutionary selection and constraint comparison. +It does not automatically repair provider dependencies or attribute bounds. + +### Rationale + +Repair changes the search distribution and can introduce hidden preferences. +For example, repairing a same-provider constraint requires deciding which +provider to preserve, potentially biasing cost or reliability. The correctness +and benefit of each repair strategy should be measured rather than assumed. + +### Consequence + +Highly constrained instances may spend many evaluations in infeasible regions. +Feasibility-aware initialization and explicit repair operators are high-priority +future experiments. + +## Decision 14: Make evaluation budgets and seeds explicit + +### Decision + +The primary termination option is `max_evaluations`, and every run accepts a +`seed`. + +### Rationale + +Evaluation count is more comparable across machines than elapsed time or +generation count. Explicit seeds make failures reproducible and enable +statistically meaningful repeated experiments. + +### Consequence + +Wall-clock and stagnation termination are not yet implemented. They should be +added as secondary limits without replacing evaluation budgets in experiments. + +## Decision 15: Return diagnostic optimization metadata + +### Decision + +Each solution includes: + +- The internal objective vector. +- Hard and soft violation totals. +- A feasibility flag. + +Provenance includes algorithm, seed, population size, evaluation budget, and +the number of returned solutions. + +### Rationale + +Evolutionary results are stochastic and cannot be assessed from the binding +alone. Diagnostic metadata is required to reproduce runs, compare algorithms, +and detect infeasible fallback results. + +### Consequence + +The response is slightly larger, especially for Pareto fronts, but remains +bounded by `archive_size`. + +## Decision 16: Keep the gateway plugin thin + +### Decision + +The gateway plugin declares capabilities, validates engine-specific essentials, +filters options, and passes the general instance through unchanged. + +### Rationale + +Duplicating a large transformation layer would make it harder to maintain +semantic equivalence and would hide information needed by future evolutionary +strategies. + +### Consequence + +The engine must remain compatible with the general schema. Schema evolution +should be handled through explicit versioning and compatibility tests. + +## Decision 17: Containerize and register the engine independently + +### Decision + +The engine has its own multi-stage Docker image, health check, registry entry, +environment variable, and Compose service. + +### Rationale + +This follows OpenBinding's engine integration model and allows independent +deployment and scaling. + +### Consequence + +Gateway startup currently depends on the evolutionary engine health in the same +way as the existing engines. If optional engine availability becomes desirable, +the broader gateway dependency policy should be changed consistently for all +engines. + +## Verification decisions + +The initial tests focus on deterministic semantic behavior: + +- Global aggregation and objective direction. +- Percentage ratio products. +- Hard/soft violation normalization. +- Provider dependency violations. +- NSGA-II and NSGA-III execution paths. +- Gateway option filtering and capability declaration. + +This is intentional: incorrect QoS or constraint semantics can produce +plausible but invalid optimization results, making them higher risk than +operator-level stochastic variation. + +## Known limitations + +- Integer SBX and polynomial mutation are generic numeric operators rather than + categorical binding-specific operators. +- There is no evaluation cache. +- There are no repair operators or feasibility-aware seeding. +- NSGA-III reference divisions are configured manually. +- The engine does not yet calculate hypervolume, IGD+, epsilon, or optimality + gaps. +- The API currently exposes only one aggregate soft-violation objective. +- Dynamic changes to candidates or QoS during a run are not supported. +- The engine assumes all required candidate feature values have passed gateway + validation. + +## Experimental questions + +The following decisions should be revisited using the experimentation suite: + +1. Does categorical uniform crossover outperform integer SBX? +2. Which mutation rate scales best with tasks and candidate-domain size? +3. When do repair operators improve time-to-feasibility without reducing front + diversity? +4. Is NSGA-III preferable to MOEA/D or RVEA for the OpenBinding many-objective + instances? +5. Should soft constraints remain aggregated or become separate objectives? +6. Which normalization strategy is most stable when valid ranges are loose? +7. When does parallel evaluation offset coordination overhead? +8. How should reference divisions adapt to objective count and archive size? + +Changes to defaults should be justified with repeated-seed evidence rather than +single-instance performance. diff --git a/engines/evolutionary-heuristics/Dockerfile b/engines/evolutionary-heuristics/Dockerfile new file mode 100644 index 0000000..56e00c7 --- /dev/null +++ b/engines/evolutionary-heuristics/Dockerfile @@ -0,0 +1,19 @@ +FROM maven:3.9.9-eclipse-temurin-21 AS builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG NO_PROXY +ENV HTTP_PROXY=$HTTP_PROXY HTTPS_PROXY=$HTTPS_PROXY NO_PROXY=$NO_PROXY +WORKDIR /app +COPY pom.xml . +RUN mvn -B dependency:go-offline +COPY src ./src +RUN mvn -B package + +FROM eclipse-temurin:21-jre +WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/evolutionary-heuristics-0.1.0-SNAPSHOT.jar /app/app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/engines/evolutionary-heuristics/README.md b/engines/evolutionary-heuristics/README.md new file mode 100644 index 0000000..39dde69 --- /dev/null +++ b/engines/evolutionary-heuristics/README.md @@ -0,0 +1,155 @@ +# Evolutionary Heuristics Engine + +This engine solves OpenBinding service-binding problems with evolutionary +algorithms. It is intentionally isolated from the legacy Java engines: it uses +Java 21, jMetal, and the general OpenBinding request model. + +The reasoning behind the architectural and algorithmic choices is recorded in +[`DESIGN_RATIONALE.md`](DESIGN_RATIONALE.md). + +## Scope + +The initial implementation supports: + +- Structured compositions: `TASK`, `SEQ`, `AND`, `XOR`, `LOOP`, and `ELEMENT`. +- Mono-, multi-, and many-objective problems. +- Global and local attribute bounds. +- Same-provider and different-provider dependency constraints. +- Hard and soft constraints. +- Reproducible runs through an explicit random seed. + +The HTTP contract is: + +- `GET /health` +- `POST /solve` with `{ "instance": , "options": {...} }` + +## Representation + +A binding is represented as a bounded integer vector. There is one gene per +abstract task and one allele per candidate service: + +```text +[candidate_index_for_task_1, ..., candidate_index_for_task_n] +``` + +This representation guarantees that every generated individual is a complete +binding and that every gene refers to an existing candidate. Task order is +derived deterministically from the composition tree. + +## Global quality evaluation + +Each individual is decoded into a task-to-candidate binding. QoS is then +aggregated recursively over the composition tree, using the functions declared +in `aggregation_policies`: + +- `SEQ` and `AND`: sum, product, minimum, maximum, or mean. +- `XOR`: probability-weighted aggregation when using sum/weighted-sum. +- `LOOP`: scale-by-iterations for additive attributes and exponentiation for + multiplicative attributes. + +Raw candidate values are never mutated during normalization. Aggregated values, +normalized values, and objective losses remain separate. + +For optimization, every target is converted to a minimization loss: + +```text +MINIMIZE: (Q - Qmin) / (Qmax - Qmin) +MAXIMIZE: 1 - (Q - Qmin) / (Qmax - Qmin) +``` + +Values are clipped to `[0, 1]`. Bounds come from an explicit min-max +normalization policy when present, otherwise from the feature `valid_range`. + +The public `objective_value` remains a weighted quality score where larger is +better. The minimization vector used by jMetal is returned in solution metadata +for diagnosis and experimentation. + +## Objective strategy + +- `MONO`: an elitist evolutionary run with one weighted loss objective. +- `MULTI`: NSGA-II, with one loss per target. +- `MANY`: NSGA-III, with one loss per target and reference-point niching. + +Weights affect the mono-objective loss and the public summary score. They do +not collapse multi- or many-objective search into a weighted sum. + +## Constraint handling + +Hard constraints use feasibility-first comparison through jMetal constraints: + +1. A feasible solution is preferred to an infeasible solution. +2. Between infeasible solutions, the lower normalized violation is preferred. +3. Objective comparison applies between feasible solutions. + +Attribute-bound violations are divided by the feature range. Dependency +violations are divided by the number of involved tasks. This prevents units +such as milliseconds and probabilities from dominating each other. + +Soft constraints are handled as follows: + +- `MONO`: `soft_penalty * normalized_soft_violation` is added to the loss. +- `MULTI` and `MANY`: normalized soft violation is appended as an additional + minimization objective. + +All violations are still reported in the returned solution. + +## Library decision + +jMetal 7.4 is used because the engine needs established implementations of +NSGA-II and NSGA-III, bounded integer solutions, constraint-aware comparison, +and a path toward standard quality indicators and experiment tooling. + +Jenetics remains a reasonable alternative for a smaller general-purpose GA, +but jMetal better matches the research-oriented multi/many-objective scope. + +## Initial configuration + +Supported options: + +```json +{ + "algorithm": "AUTO", + "population_size": 100, + "max_evaluations": 10000, + "crossover_probability": 0.9, + "mutation_probability": null, + "distribution_index": 20.0, + "archive_size": 100, + "soft_penalty": 10.0, + "seed": 1, + "reference_divisions": 12 +} +``` + +When mutation probability is omitted, it is set to `1 / number_of_tasks`. +`AUTO` selects NSGA-II for `MONO`/`MULTI` and NSGA-III for `MANY`. + +## Planned refinements + +The first implementation establishes the full evaluation and optimization +pipeline. The following refinements should be evaluated experimentally before +being enabled by default: + +- Feasibility-aware population seeding. +- Repair operators for local bounds and provider dependencies. +- A discrete uniform crossover instead of integer SBX. +- Adaptive penalties and epsilon constraint handling. +- Parallel evaluation for large populations. +- Hypervolume, IGD+, epsilon, and optimality-gap reports. +- Memoization of repeated chromosomes. +- Termination by wall-clock budget and stagnation. + +## Experimental protocol + +Compare against MiniZinc, random search, and the existing many-objective +heuristic using: + +- Feasibility rate. +- Optimality gap for instances with an exact solution. +- Hypervolume, IGD+, and epsilon for Pareto fronts. +- Evaluations, runtime, and memory. +- At least 20 independent seeds. +- Scaling by tasks, candidates, objectives, and constraints. + +The primary budget should be number of evaluations, because it is more +comparable across machines than generations or elapsed time. diff --git a/engines/evolutionary-heuristics/pom.xml b/engines/evolutionary-heuristics/pom.xml new file mode 100644 index 0000000..0bf4bcb --- /dev/null +++ b/engines/evolutionary-heuristics/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + es.us.isa.openbinding + evolutionary-heuristics + 0.1.0-SNAPSHOT + + + 21 + UTF-8 + 7.4 + 5.12.2 + + + + + org.uma.jmetal + jmetal-core + ${jmetal.version} + + + org.uma.jmetal + jmetal-algorithm + ${jmetal.version} + + + com.google.code.gson + gson + 2.13.1 + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + shade + + false + + + es.us.isa.openbinding.evolutionary.Server + + + + + + + + + diff --git a/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/ApiModels.java b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/ApiModels.java new file mode 100644 index 0000000..7b43da9 --- /dev/null +++ b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/ApiModels.java @@ -0,0 +1,147 @@ +package es.us.isa.openbinding.evolutionary; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class ApiModels { + private ApiModels() {} + + static final class SolveRequest { + Instance instance; + Options options = new Options(); + } + + static final class Instance { + Metadata metadata; + List features = new ArrayList<>(); + List candidates = new ArrayList<>(); + Composition composition; + Map aggregation_policies = new LinkedHashMap<>(); + List constraints = new ArrayList<>(); + Objective objective; + } + + static final class Metadata { + String id; + } + + static final class Feature { + String id; + String direction; + String scale; + NumericRange valid_range; + } + + static final class NumericRange { + double min; + double max; + } + + static final class Candidate { + String id; + String task_id; + String provider_id; + Map features = new LinkedHashMap<>(); + } + + static final class Composition { + String type; + Node root; + } + + static final class Node { + String id; + String kind; + String task_id; + List children; + List branches; + Node body; + Double expected_iterations; + NumericRange bounds; + } + + static final class Branch { + double p; + Node child; + } + + static final class AggregationPolicy { + Double neutral; + Map compose = new LinkedHashMap<>(); + Normalization normalize; + } + + static final class AggregationFunction { + String fn; + } + + static final class Normalization { + String type; + NumericRange bounds; + Boolean increasing_is_better; + } + + static final class Constraint { + String id; + String kind; + String scope; + String attribute_id; + String op; + Object value; + List tasks = new ArrayList<>(); + String type; + Boolean hard; + + boolean isHard() { + return hard == null || hard; + } + } + + static final class Objective { + String type; + List targets = new ArrayList<>(); + Map weights = new LinkedHashMap<>(); + } + + static final class Options { + String algorithm = "AUTO"; + int population_size = 100; + int max_evaluations = 10_000; + double crossover_probability = 0.9; + Double mutation_probability; + double distribution_index = 20.0; + int archive_size = 100; + double soft_penalty = 10.0; + long seed = 1L; + int reference_divisions = 12; + } + + static final class SolveResponse { + List solutions = new ArrayList<>(); + Provenance provenance = new Provenance(); + } + + static final class SolutionDto { + double objective_value; + Map binding = new LinkedHashMap<>(); + Map aggregated_features = new LinkedHashMap<>(); + List violations = new ArrayList<>(); + Map metadata = new LinkedHashMap<>(); + } + + static final class ViolationDto { + String constraint_id; + String message; + String code = "constraint_violation"; + double penalty; + String description; + } + + static final class Provenance { + String engine_id = "evolutionary-heuristics"; + long execution_time_ms; + Map metadata = new LinkedHashMap<>(); + } +} diff --git a/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/BindingEvaluator.java b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/BindingEvaluator.java new file mode 100644 index 0000000..a7e24df --- /dev/null +++ b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/BindingEvaluator.java @@ -0,0 +1,388 @@ +package es.us.isa.openbinding.evolutionary; + +import static es.us.isa.openbinding.evolutionary.ApiModels.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +final class BindingEvaluator { + record ConstraintEvaluation(double hardViolation, double softViolation, List violations) {} + record Evaluation( + Map binding, + Map aggregated, + Map losses, + ConstraintEvaluation constraints) {} + + private final Instance instance; + private final List taskIds; + private final Map> candidatesByTask; + private final Map features; + + BindingEvaluator(Instance instance) { + this.instance = instance; + this.taskIds = collectTaskIds(instance.composition.root); + this.candidatesByTask = groupCandidates(instance.candidates); + this.features = new LinkedHashMap<>(); + for (Feature feature : instance.features) { + features.put(feature.id, feature); + } + for (String taskId : taskIds) { + if (!candidatesByTask.containsKey(taskId) || candidatesByTask.get(taskId).isEmpty()) { + throw new IllegalArgumentException("No candidates available for task '" + taskId + "'"); + } + } + } + + List taskIds() { + return taskIds; + } + + int candidateCount(int taskIndex) { + return candidatesByTask.get(taskIds.get(taskIndex)).size(); + } + + Evaluation evaluate(List chromosome) { + Map selected = new LinkedHashMap<>(); + Map binding = new LinkedHashMap<>(); + for (int i = 0; i < taskIds.size(); i++) { + String taskId = taskIds.get(i); + Candidate candidate = candidatesByTask.get(taskId).get(chromosome.get(i)); + selected.put(taskId, candidate); + binding.put(taskId, candidate.id); + } + + Map aggregated = new LinkedHashMap<>(); + Map losses = new LinkedHashMap<>(); + for (Feature feature : instance.features) { + double raw = fromCompositionValue(feature, aggregate(instance.composition.root, feature, selected)); + aggregated.put(feature.id, raw); + losses.put(feature.id, objectiveLoss(feature, raw)); + } + + return new Evaluation(binding, aggregated, losses, evaluateConstraints(selected, aggregated)); + } + + double qualityScore(Evaluation evaluation) { + double score = 0.0; + double totalWeight = 0.0; + for (String target : instance.objective.targets) { + double weight = instance.objective.weights.getOrDefault(target, 1.0); + score += weight * (1.0 - evaluation.losses.getOrDefault(target, 1.0)); + totalWeight += weight; + } + return totalWeight > 0.0 ? score / totalWeight : 0.0; + } + + private double aggregate(Node node, Feature feature, Map selected) { + String kind = upper(node.kind); + return switch (kind) { + case "TASK" -> toCompositionValue( + feature, selected.get(node.task_id).features.getOrDefault(feature.id, neutral(feature))); + case "ELEMENT" -> toCompositionValue(feature, neutral(feature)); + case "SEQ", "AND" -> aggregateChildren(node.children, feature, selected, function(feature, kind)); + case "XOR" -> aggregateXor(node, feature, selected); + case "LOOP" -> aggregateLoop(node, feature, selected); + default -> throw new IllegalArgumentException("Unsupported composition node: " + node.kind); + }; + } + + private double aggregateChildren( + List children, Feature feature, Map selected, String function) { + List values = new ArrayList<>(); + if (children != null) { + for (Node child : children) { + values.add(aggregate(child, feature, selected)); + } + } + return aggregateValues(values, null, function, toCompositionValue(feature, neutral(feature))); + } + + private double aggregateXor(Node node, Feature feature, Map selected) { + List values = new ArrayList<>(); + List weights = new ArrayList<>(); + if (node.branches != null) { + for (Branch branch : node.branches) { + values.add(aggregate(branch.child, feature, selected)); + weights.add(branch.p); + } + } + String fn = function(feature, "XOR"); + if (fn.equals("SUM") || fn.equals("WEIGHTED_SUM") || fn.equals("SCALED_SUM")) { + return aggregateValues( + values, weights, "WEIGHTED_SUM", toCompositionValue(feature, neutral(feature))); + } + return aggregateValues(values, null, fn, toCompositionValue(feature, neutral(feature))); + } + + private double aggregateLoop(Node node, Feature feature, Map selected) { + double value = aggregate(node.body, feature, selected); + double iterations = node.expected_iterations != null + ? node.expected_iterations + : node.bounds != null ? (node.bounds.min + node.bounds.max) / 2.0 : 1.0; + String fn = function(feature, "LOOP"); + if (fn.contains("PRODUCT")) { + return Math.pow(value, iterations); + } + if (fn.contains("SUM") || fn.contains("SCALE")) { + return value * iterations; + } + return value; + } + + private double aggregateValues( + List values, List weights, String function, double neutral) { + if (values.isEmpty()) { + return neutral; + } + return switch (function) { + case "PRODUCT", "SCALED_PRODUCT" -> values.stream().reduce(1.0, (a, b) -> a * b); + case "MAX", "SCALED_MAX" -> values.stream().mapToDouble(Double::doubleValue).max().orElse(neutral); + case "MIN", "SCALED_MIN" -> values.stream().mapToDouble(Double::doubleValue).min().orElse(neutral); + case "MEAN", "AVERAGE" -> values.stream().mapToDouble(Double::doubleValue).average().orElse(neutral); + case "WEIGHTED_SUM" -> { + double result = 0.0; + for (int i = 0; i < values.size(); i++) { + result += values.get(i) * weights.get(i); + } + yield result; + } + default -> values.stream().mapToDouble(Double::doubleValue).sum(); + }; + } + + private double objectiveLoss(Feature feature, double raw) { + NumericRange range = normalizationRange(feature); + if (range == null || Math.abs(range.max - range.min) < 1e-12) { + return 0.0; + } + double normalized = clamp((raw - range.min) / (range.max - range.min)); + return upper(feature.direction).equals("MAXIMIZE") ? 1.0 - normalized : normalized; + } + + private double toCompositionValue(Feature feature, double raw) { + double denominator = productRatioDenominator(feature); + if (denominator <= 1.0 || (raw >= 0.0 && raw <= 1.0)) { + return raw; + } + return raw / denominator; + } + + private double fromCompositionValue(Feature feature, double value) { + double denominator = productRatioDenominator(feature); + return denominator <= 1.0 ? value : value * denominator; + } + + private double productRatioDenominator(Feature feature) { + if (!"RATIO".equals(upper(feature.scale)) + || feature.valid_range == null + || feature.valid_range.max <= 1.0 + || !usesProductSpace(feature)) { + return 1.0; + } + return feature.valid_range.max; + } + + private boolean usesProductSpace(Feature feature) { + AggregationPolicy policy = instance.aggregation_policies.get(feature.id); + if (policy == null || policy.compose == null) { + return false; + } + return policy.compose.values().stream() + .anyMatch(fn -> fn != null && upper(fn.fn).contains("PRODUCT")); + } + + private ConstraintEvaluation evaluateConstraints( + Map selected, Map aggregated) { + double hard = 0.0; + double soft = 0.0; + List violations = new ArrayList<>(); + + for (Constraint constraint : instance.constraints) { + double violation = constraintViolation(constraint, selected, aggregated); + if (violation <= 0.0) { + continue; + } + if (constraint.isHard()) { + hard += violation; + } else { + soft += violation; + } + ViolationDto dto = new ViolationDto(); + dto.constraint_id = constraint.id; + dto.message = "Constraint " + constraint.id + " violated"; + dto.penalty = violation; + dto.description = "Normalized violation: " + violation; + violations.add(dto); + } + return new ConstraintEvaluation(hard, soft, violations); + } + + private double constraintViolation( + Constraint constraint, Map selected, Map aggregated) { + if (upper(constraint.kind).equals("DEPENDENCY")) { + return dependencyViolation(constraint, selected); + } + if (!upper(constraint.kind).equals("ATTRIBUTE_BOUND")) { + return 0.0; + } + + Feature feature = features.get(constraint.attribute_id); + if (feature == null) { + return 1.0; + } + double scale = featureRange(feature); + if (upper(constraint.scope).equals("LOCAL")) { + double sum = 0.0; + for (String task : constraint.tasks) { + Candidate candidate = selected.get(task); + if (candidate == null) { + sum += 1.0; + } else { + sum += boundViolation( + candidate.features.getOrDefault(feature.id, neutral(feature)), constraint) / scale; + } + } + return constraint.tasks.isEmpty() ? 0.0 : sum / constraint.tasks.size(); + } + return boundViolation(aggregated.getOrDefault(feature.id, neutral(feature)), constraint) / scale; + } + + private double dependencyViolation(Constraint constraint, Map selected) { + Set providers = new LinkedHashSet<>(); + for (String task : constraint.tasks) { + Candidate candidate = selected.get(task); + if (candidate != null) { + providers.add(candidate.provider_id != null ? candidate.provider_id : candidate.id); + } + } + int taskCount = Math.max(1, constraint.tasks.size()); + if (upper(constraint.type).equals("SAME_PROVIDER")) { + return Math.max(0, providers.size() - 1) / (double) taskCount; + } + return Math.max(0, constraint.tasks.size() - providers.size()) / (double) taskCount; + } + + private double boundViolation(double current, Constraint constraint) { + if (upper(constraint.op).equals("IN_RANGE")) { + NumericRange range = rangeValue(constraint.value); + if (range == null) { + return 1.0; + } + return current < range.min ? range.min - current : Math.max(0.0, current - range.max); + } + double target = numberValue(constraint.value); + return switch (constraint.op) { + case "<=" -> Math.max(0.0, current - target); + case "<" -> current < target ? 0.0 : current - target + 1e-12; + case ">=" -> Math.max(0.0, target - current); + case ">" -> current > target ? 0.0 : target - current + 1e-12; + case "==" -> Math.abs(current - target); + case "!=" -> Math.abs(current - target) < 1e-12 ? 1.0 : 0.0; + default -> 0.0; + }; + } + + private NumericRange rangeValue(Object value) { + if (!(value instanceof Map map)) { + return null; + } + NumericRange range = new NumericRange(); + range.min = ((Number) map.get("min")).doubleValue(); + range.max = ((Number) map.get("max")).doubleValue(); + return range; + } + + private double numberValue(Object value) { + if (value instanceof Number number) { + return number.doubleValue(); + } + throw new IllegalArgumentException("Constraint value must be numeric"); + } + + private NumericRange normalizationRange(Feature feature) { + AggregationPolicy policy = instance.aggregation_policies.get(feature.id); + if (policy != null && policy.normalize != null + && "MINMAX".equals(upper(policy.normalize.type)) && policy.normalize.bounds != null) { + return policy.normalize.bounds; + } + return feature.valid_range; + } + + private double featureRange(Feature feature) { + NumericRange range = normalizationRange(feature); + return range == null ? 1.0 : Math.max(1e-12, Math.abs(range.max - range.min)); + } + + private double neutral(Feature feature) { + AggregationPolicy policy = instance.aggregation_policies.get(feature.id); + if (policy != null && policy.neutral != null) { + return policy.neutral; + } + return upper(feature.direction).equals("MAXIMIZE") + ? feature.valid_range.min + : feature.valid_range.max; + } + + private String function(Feature feature, String nodeKind) { + AggregationPolicy policy = instance.aggregation_policies.get(feature.id); + if (policy == null || policy.compose == null) { + return nodeKind.equals("AND") ? "MAX" : "SUM"; + } + String key = switch (nodeKind) { + case "AND" -> "and"; + case "XOR" -> "xor"; + case "LOOP" -> "loop"; + default -> "seq"; + }; + AggregationFunction fn = policy.compose.get(key); + return fn == null || fn.fn == null ? (nodeKind.equals("AND") ? "MAX" : "SUM") : upper(fn.fn); + } + + private static Map> groupCandidates(List candidates) { + Map> result = new LinkedHashMap<>(); + for (Candidate candidate : candidates) { + result.computeIfAbsent(candidate.task_id, ignored -> new ArrayList<>()).add(candidate); + } + return result; + } + + private static List collectTaskIds(Node root) { + Set result = new LinkedHashSet<>(); + collectTaskIds(root, result); + return new ArrayList<>(result); + } + + private static void collectTaskIds(Node node, Set result) { + if (node == null) { + return; + } + switch (upper(node.kind)) { + case "TASK" -> result.add(node.task_id); + case "SEQ", "AND" -> { + if (node.children != null) { + node.children.forEach(child -> collectTaskIds(child, result)); + } + } + case "XOR" -> { + if (node.branches != null) { + node.branches.forEach(branch -> collectTaskIds(branch.child, result)); + } + } + case "LOOP" -> collectTaskIds(node.body, result); + default -> {} + } + } + + private static double clamp(double value) { + return Math.max(0.0, Math.min(1.0, value)); + } + + private static String upper(String value) { + return value == null ? "" : value.toUpperCase(Locale.ROOT); + } +} diff --git a/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/EvolutionaryBindingProblem.java b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/EvolutionaryBindingProblem.java new file mode 100644 index 0000000..33141df --- /dev/null +++ b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/EvolutionaryBindingProblem.java @@ -0,0 +1,77 @@ +package es.us.isa.openbinding.evolutionary; + +import static es.us.isa.openbinding.evolutionary.ApiModels.*; + +import java.util.ArrayList; +import java.util.List; +import org.uma.jmetal.problem.integerproblem.impl.AbstractIntegerProblem; +import org.uma.jmetal.solution.integersolution.IntegerSolution; + +final class EvolutionaryBindingProblem extends AbstractIntegerProblem { + static final String EVALUATION_ATTRIBUTE = "openbinding.evaluation"; + + private final BindingEvaluator evaluator; + private final Instance instance; + private final Options options; + private final boolean hasSoftConstraints; + + EvolutionaryBindingProblem(Instance instance, Options options) { + this.instance = instance; + this.options = options; + this.evaluator = new BindingEvaluator(instance); + this.hasSoftConstraints = instance.constraints.stream().anyMatch(c -> !c.isHard()); + + name("OpenBindingEvolutionaryProblem"); + numberOfObjectives(baseObjectiveCount() + (appendSoftObjective() ? 1 : 0)); + numberOfConstraints(1); + + List lowerBounds = new ArrayList<>(); + List upperBounds = new ArrayList<>(); + for (int i = 0; i < evaluator.taskIds().size(); i++) { + lowerBounds.add(0); + upperBounds.add(evaluator.candidateCount(i) - 1); + } + variableBounds(lowerBounds, upperBounds); + } + + @Override + public IntegerSolution evaluate(IntegerSolution solution) { + BindingEvaluator.Evaluation evaluation = evaluator.evaluate(solution.variables()); + solution.attributes().put(EVALUATION_ATTRIBUTE, evaluation); + + if ("MONO".equalsIgnoreCase(instance.objective.type)) { + double weightedLoss = 0.0; + double totalWeight = 0.0; + for (String target : instance.objective.targets) { + double weight = instance.objective.weights.getOrDefault(target, 1.0); + weightedLoss += weight * evaluation.losses().getOrDefault(target, 1.0); + totalWeight += weight; + } + solution.objectives()[0] = (totalWeight > 0.0 ? weightedLoss / totalWeight : weightedLoss) + + options.soft_penalty * evaluation.constraints().softViolation(); + } else { + int index = 0; + for (String target : instance.objective.targets) { + solution.objectives()[index++] = evaluation.losses().getOrDefault(target, 1.0); + } + if (appendSoftObjective()) { + solution.objectives()[index] = evaluation.constraints().softViolation(); + } + } + + solution.constraints()[0] = -evaluation.constraints().hardViolation(); + return solution; + } + + BindingEvaluator evaluator() { + return evaluator; + } + + private int baseObjectiveCount() { + return "MONO".equalsIgnoreCase(instance.objective.type) ? 1 : instance.objective.targets.size(); + } + + private boolean appendSoftObjective() { + return hasSoftConstraints && !"MONO".equalsIgnoreCase(instance.objective.type); + } +} diff --git a/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/EvolutionarySolver.java b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/EvolutionarySolver.java new file mode 100644 index 0000000..90ed690 --- /dev/null +++ b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/EvolutionarySolver.java @@ -0,0 +1,145 @@ +package es.us.isa.openbinding.evolutionary; + +import static es.us.isa.openbinding.evolutionary.ApiModels.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import org.uma.jmetal.algorithm.Algorithm; +import org.uma.jmetal.algorithm.multiobjective.nsgaii.NSGAIIBuilder; +import org.uma.jmetal.algorithm.multiobjective.nsgaiii.NSGAIIIBuilder; +import org.uma.jmetal.operator.crossover.impl.IntegerSBXCrossover; +import org.uma.jmetal.operator.mutation.impl.IntegerPolynomialMutation; +import org.uma.jmetal.operator.selection.impl.BinaryTournamentSelection; +import org.uma.jmetal.solution.integersolution.IntegerSolution; +import org.uma.jmetal.util.SolutionListUtils; +import org.uma.jmetal.util.pseudorandom.JMetalRandom; + +final class EvolutionarySolver { + SolveResponse solve(SolveRequest request) { + validate(request); + Options options = request.options == null ? new Options() : request.options; + EvolutionaryBindingProblem problem = new EvolutionaryBindingProblem(request.instance, options); + + double mutationProbability = options.mutation_probability != null + ? options.mutation_probability + : 1.0 / Math.max(1, problem.numberOfVariables()); + IntegerSBXCrossover crossover = + new IntegerSBXCrossover(options.crossover_probability, options.distribution_index); + IntegerPolynomialMutation mutation = + new IntegerPolynomialMutation(mutationProbability, options.distribution_index); + + JMetalRandom.getInstance().setSeed(options.seed); + String algorithmName = resolveAlgorithm(request.instance.objective.type, options.algorithm); + Algorithm> algorithm; + if ("NSGAIII".equals(algorithmName)) { + int iterations = Math.max(1, options.max_evaluations / Math.max(1, options.population_size)); + algorithm = new NSGAIIIBuilder(problem) + .setMaxIterations(iterations) + .setNumberOfDivisions(options.reference_divisions) + .setCrossoverOperator(crossover) + .setMutationOperator(mutation) + .setSelectionOperator(new BinaryTournamentSelection<>()) + .build(); + } else { + algorithm = new NSGAIIBuilder(problem, crossover, mutation, options.population_size) + .setMaxEvaluations(options.max_evaluations) + .build(); + } + + long started = System.currentTimeMillis(); + algorithm.run(); + List result = algorithm.result(); + long elapsed = System.currentTimeMillis() - started; + + List selected = selectResult(result, request.instance.objective.type, options.archive_size); + SolveResponse response = new SolveResponse(); + for (IntegerSolution solution : selected) { + response.solutions.add(toDto(solution, problem)); + } + response.provenance.execution_time_ms = elapsed; + response.provenance.metadata.put("algorithm", algorithmName); + response.provenance.metadata.put("seed", options.seed); + response.provenance.metadata.put("population_size", options.population_size); + response.provenance.metadata.put("max_evaluations", options.max_evaluations); + response.provenance.metadata.put("returned_solutions", response.solutions.size()); + return response; + } + + private List selectResult( + List result, String objectiveType, int archiveSize) { + if ("MONO".equalsIgnoreCase(objectiveType)) { + return result.stream() + .filter(this::feasible) + .min(Comparator.comparingDouble(s -> s.objectives()[0])) + .map(List::of) + .orElseGet(() -> result.stream() + .min(Comparator.comparingDouble(s -> Math.abs(s.constraints()[0]))) + .map(List::of) + .orElseGet(List::of)); + } + List feasibleSolutions = result.stream().filter(this::feasible).toList(); + List candidates = feasibleSolutions.isEmpty() + ? result.stream() + .sorted(Comparator.comparingDouble(s -> Math.abs(s.constraints()[0]))) + .limit(Math.max(1, archiveSize)) + .toList() + : feasibleSolutions; + List front = SolutionListUtils.getNonDominatedSolutions(candidates); + front.sort(Comparator.comparingDouble(this::objectiveSum)); + return new ArrayList<>(front.subList(0, Math.min(Math.max(1, archiveSize), front.size()))); + } + + private SolutionDto toDto(IntegerSolution solution, EvolutionaryBindingProblem problem) { + BindingEvaluator.Evaluation evaluation = + (BindingEvaluator.Evaluation) solution.attributes().get(EvolutionaryBindingProblem.EVALUATION_ATTRIBUTE); + if (evaluation == null) { + problem.evaluate(solution); + evaluation = + (BindingEvaluator.Evaluation) solution.attributes().get(EvolutionaryBindingProblem.EVALUATION_ATTRIBUTE); + } + + SolutionDto dto = new SolutionDto(); + dto.binding.putAll(evaluation.binding()); + dto.aggregated_features.putAll(evaluation.aggregated()); + dto.violations.addAll(evaluation.constraints().violations()); + dto.objective_value = problem.evaluator().qualityScore(evaluation); + dto.metadata.put("objective_vector", Arrays.stream(solution.objectives()).boxed().toList()); + dto.metadata.put("hard_violation", evaluation.constraints().hardViolation()); + dto.metadata.put("soft_violation", evaluation.constraints().softViolation()); + dto.metadata.put("feasible", feasible(solution)); + return dto; + } + + private boolean feasible(IntegerSolution solution) { + return solution.constraints().length == 0 || solution.constraints()[0] >= 0.0; + } + + private double objectiveSum(IntegerSolution solution) { + return Arrays.stream(solution.objectives()).sum(); + } + + private String resolveAlgorithm(String objectiveType, String configured) { + if (configured != null && !"AUTO".equalsIgnoreCase(configured)) { + String normalized = configured.replace("-", "").toUpperCase(); + if (!normalized.equals("NSGAII") && !normalized.equals("NSGAIII")) { + throw new IllegalArgumentException("algorithm must be AUTO, NSGAII, or NSGAIII"); + } + return normalized; + } + return "MANY".equalsIgnoreCase(objectiveType) ? "NSGAIII" : "NSGAII"; + } + + private void validate(SolveRequest request) { + if (request == null || request.instance == null) { + throw new IllegalArgumentException("Missing OpenBinding instance"); + } + if (request.instance.objective == null || request.instance.objective.targets.isEmpty()) { + throw new IllegalArgumentException("At least one objective target is required"); + } + if (request.instance.composition == null || request.instance.composition.root == null) { + throw new IllegalArgumentException("A structured composition root is required"); + } + } +} diff --git a/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/Server.java b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/Server.java new file mode 100644 index 0000000..d42176c --- /dev/null +++ b/engines/evolutionary-heuristics/src/main/java/es/us/isa/openbinding/evolutionary/Server.java @@ -0,0 +1,57 @@ +package es.us.isa.openbinding.evolutionary; + +import static es.us.isa.openbinding.evolutionary.ApiModels.*; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.Executors; + +public final class Server { + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); + private static final int MAX_BODY_BYTES = 512 * 1024 * 1024; + + private Server() {} + + public static void main(String[] args) throws IOException { + int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080")); + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/health", exchange -> writeJson(exchange, 200, Map.of("status", "ok"))); + server.createContext("/solve", Server::solve); + server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); + server.start(); + } + + private static void solve(HttpExchange exchange) throws IOException { + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + writeJson(exchange, 405, Map.of("error", "Method not allowed")); + return; + } + try { + byte[] body = exchange.getRequestBody().readNBytes(MAX_BODY_BYTES + 1); + if (body.length > MAX_BODY_BYTES) { + writeJson(exchange, 413, Map.of("error", "Request body is too large")); + return; + } + SolveRequest request = GSON.fromJson(new String(body, StandardCharsets.UTF_8), SolveRequest.class); + writeJson(exchange, 200, new EvolutionarySolver().solve(request)); + } catch (IllegalArgumentException exception) { + writeJson(exchange, 422, Map.of("error", exception.getMessage())); + } catch (Exception exception) { + writeJson(exchange, 500, Map.of("error", exception.getMessage())); + } + } + + private static void writeJson(HttpExchange exchange, int status, Object payload) throws IOException { + byte[] body = GSON.toJson(payload).getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(status, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + } +} diff --git a/engines/evolutionary-heuristics/src/test/java/es/us/isa/openbinding/evolutionary/BindingEvaluatorTest.java b/engines/evolutionary-heuristics/src/test/java/es/us/isa/openbinding/evolutionary/BindingEvaluatorTest.java new file mode 100644 index 0000000..d071143 --- /dev/null +++ b/engines/evolutionary-heuristics/src/test/java/es/us/isa/openbinding/evolutionary/BindingEvaluatorTest.java @@ -0,0 +1,187 @@ +package es.us.isa.openbinding.evolutionary; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import java.util.List; +import org.junit.jupiter.api.Test; + +class BindingEvaluatorTest { + private static final Gson GSON = new Gson(); + + @Test + void aggregatesGlobalQualityAndOrientsLosses() { + ApiModels.SolveRequest request = parse(""" + { + "instance": { + "features": [ + {"id":"cost","direction":"MINIMIZE","valid_range":{"min":0,"max":100}}, + {"id":"reliability","direction":"MAXIMIZE","valid_range":{"min":0,"max":1}} + ], + "candidates": [ + {"id":"a1","task_id":"a","provider_id":"p1","features":{"cost":10,"reliability":0.9}}, + {"id":"b1","task_id":"b","provider_id":"p1","features":{"cost":20,"reliability":0.8}} + ], + "composition": { + "type":"STRUCTURED", + "root":{"kind":"SEQ","children":[ + {"kind":"TASK","task_id":"a"}, + {"kind":"TASK","task_id":"b"} + ]} + }, + "aggregation_policies": { + "cost":{"neutral":0,"compose":{"seq":{"fn":"SUM"}}}, + "reliability":{"neutral":1,"compose":{"seq":{"fn":"PRODUCT"}}} + }, + "constraints": [], + "objective": { + "type":"MULTI", + "targets":["cost","reliability"], + "weights":{"cost":0.5,"reliability":0.5} + } + } + } + """); + + BindingEvaluator.Evaluation evaluation = + new BindingEvaluator(request.instance).evaluate(List.of(0, 0)); + + assertEquals(30.0, evaluation.aggregated().get("cost"), 1e-12); + assertEquals(0.72, evaluation.aggregated().get("reliability"), 1e-12); + assertEquals(0.30, evaluation.losses().get("cost"), 1e-12); + assertEquals(0.28, evaluation.losses().get("reliability"), 1e-12); + } + + @Test + void separatesNormalizedHardAndSoftViolations() { + ApiModels.SolveRequest request = parse(""" + { + "instance": { + "features": [ + {"id":"latency","direction":"MINIMIZE","valid_range":{"min":0,"max":100}} + ], + "candidates": [ + {"id":"slow","task_id":"a","provider_id":"p1","features":{"latency":60}}, + {"id":"other","task_id":"b","provider_id":"p1","features":{"latency":10}} + ], + "composition": { + "type":"STRUCTURED", + "root":{"kind":"SEQ","children":[ + {"kind":"TASK","task_id":"a"}, + {"kind":"TASK","task_id":"b"} + ]} + }, + "aggregation_policies": { + "latency":{"neutral":0,"compose":{"seq":{"fn":"SUM"}}} + }, + "constraints": [ + { + "id":"hard-global","kind":"ATTRIBUTE_BOUND","scope":"GLOBAL", + "attribute_id":"latency","op":"<=","value":50,"hard":true + }, + { + "id":"soft-local","kind":"ATTRIBUTE_BOUND","scope":"LOCAL", + "attribute_id":"latency","op":"<=","value":40,"tasks":["a"],"hard":false + } + ], + "objective": { + "type":"MONO","targets":["latency"],"weights":{"latency":1.0} + } + } + } + """); + + BindingEvaluator.Evaluation evaluation = + new BindingEvaluator(request.instance).evaluate(List.of(0, 0)); + + assertEquals(0.20, evaluation.constraints().hardViolation(), 1e-12); + assertEquals(0.20, evaluation.constraints().softViolation(), 1e-12); + assertEquals(2, evaluation.constraints().violations().size()); + } + + @Test + void evaluatesProviderDependencies() { + ApiModels.SolveRequest request = parse(""" + { + "instance": { + "features": [ + {"id":"cost","direction":"MINIMIZE","valid_range":{"min":0,"max":10}} + ], + "candidates": [ + {"id":"a1","task_id":"a","provider_id":"p1","features":{"cost":1}}, + {"id":"b1","task_id":"b","provider_id":"p2","features":{"cost":1}} + ], + "composition": { + "type":"STRUCTURED", + "root":{"kind":"SEQ","children":[ + {"kind":"TASK","task_id":"a"}, + {"kind":"TASK","task_id":"b"} + ]} + }, + "aggregation_policies": { + "cost":{"neutral":0,"compose":{"seq":{"fn":"SUM"}}} + }, + "constraints": [ + { + "id":"same-provider","kind":"DEPENDENCY","type":"SAME_PROVIDER", + "tasks":["a","b"],"hard":true + } + ], + "objective": { + "type":"MONO","targets":["cost"],"weights":{"cost":1.0} + } + } + } + """); + + BindingEvaluator.Evaluation evaluation = + new BindingEvaluator(request.instance).evaluate(List.of(0, 0)); + + assertEquals(0.5, evaluation.constraints().hardViolation(), 1e-12); + assertEquals(0.0, evaluation.constraints().softViolation(), 1e-12); + } + + @Test + void aggregatesPercentageRatiosInProductSpace() { + ApiModels.SolveRequest request = parse(""" + { + "instance": { + "features": [ + { + "id":"availability","direction":"MAXIMIZE","scale":"RATIO", + "valid_range":{"min":0,"max":100} + } + ], + "candidates": [ + {"id":"a1","task_id":"a","features":{"availability":99}}, + {"id":"b1","task_id":"b","features":{"availability":98}} + ], + "composition": { + "type":"STRUCTURED", + "root":{"kind":"SEQ","children":[ + {"kind":"TASK","task_id":"a"}, + {"kind":"TASK","task_id":"b"} + ]} + }, + "aggregation_policies": { + "availability":{"neutral":1,"compose":{"seq":{"fn":"PRODUCT"}}} + }, + "constraints": [], + "objective": { + "type":"MONO","targets":["availability"],"weights":{"availability":1.0} + } + } + } + """); + + BindingEvaluator.Evaluation evaluation = + new BindingEvaluator(request.instance).evaluate(List.of(0, 0)); + + assertEquals(97.02, evaluation.aggregated().get("availability"), 1e-12); + assertEquals(0.0298, evaluation.losses().get("availability"), 1e-12); + } + + private ApiModels.SolveRequest parse(String json) { + return GSON.fromJson(json, ApiModels.SolveRequest.class); + } +} diff --git a/engines/evolutionary-heuristics/src/test/java/es/us/isa/openbinding/evolutionary/EvolutionarySolverTest.java b/engines/evolutionary-heuristics/src/test/java/es/us/isa/openbinding/evolutionary/EvolutionarySolverTest.java new file mode 100644 index 0000000..7792b0c --- /dev/null +++ b/engines/evolutionary-heuristics/src/test/java/es/us/isa/openbinding/evolutionary/EvolutionarySolverTest.java @@ -0,0 +1,83 @@ +package es.us.isa.openbinding.evolutionary; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +class EvolutionarySolverTest { + private static final Gson GSON = new Gson(); + + @Test + void runsNsgaIIForMonoObjective() { + ApiModels.SolveRequest request = request("MONO", """ + ["cost"] + """, """ + {"cost":1.0} + """); + + ApiModels.SolveResponse response = new EvolutionarySolver().solve(request); + + assertFalse(response.solutions.isEmpty()); + assertEquals("NSGAII", response.provenance.metadata.get("algorithm")); + } + + @Test + void runsNsgaIIIForManyObjectives() { + ApiModels.SolveRequest request = request("MANY", """ + ["cost","latency","reliability"] + """, """ + {"cost":0.34,"latency":0.33,"reliability":0.33} + """); + + ApiModels.SolveResponse response = new EvolutionarySolver().solve(request); + + assertFalse(response.solutions.isEmpty()); + assertEquals("NSGAIII", response.provenance.metadata.get("algorithm")); + } + + private ApiModels.SolveRequest request(String type, String targets, String weights) { + String json = """ + { + "instance": { + "features": [ + {"id":"cost","direction":"MINIMIZE","valid_range":{"min":0,"max":100}}, + {"id":"latency","direction":"MINIMIZE","valid_range":{"min":0,"max":100}}, + {"id":"reliability","direction":"MAXIMIZE","valid_range":{"min":0,"max":1}} + ], + "candidates": [ + {"id":"a1","task_id":"a","features":{"cost":10,"latency":30,"reliability":0.9}}, + {"id":"a2","task_id":"a","features":{"cost":30,"latency":10,"reliability":0.99}}, + {"id":"b1","task_id":"b","features":{"cost":20,"latency":20,"reliability":0.95}}, + {"id":"b2","task_id":"b","features":{"cost":5,"latency":50,"reliability":0.8}} + ], + "composition": { + "type":"STRUCTURED", + "root":{"kind":"SEQ","children":[ + {"kind":"TASK","task_id":"a"}, + {"kind":"TASK","task_id":"b"} + ]} + }, + "aggregation_policies": { + "cost":{"neutral":0,"compose":{"seq":{"fn":"SUM"}}}, + "latency":{"neutral":0,"compose":{"seq":{"fn":"SUM"}}}, + "reliability":{"neutral":1,"compose":{"seq":{"fn":"PRODUCT"}}} + }, + "constraints": [], + "objective": { + "type":"%s","targets":%s,"weights":%s + } + }, + "options": { + "population_size":20, + "max_evaluations":100, + "archive_size":10, + "seed":7, + "reference_divisions":4 + } + } + """.formatted(type, targets, weights); + return GSON.fromJson(json, ApiModels.SolveRequest.class); + } +} diff --git a/engines/minizinc-csp/package.json b/engines/minizinc-csp/package.json index b12e88f..77d84b3 100644 --- a/engines/minizinc-csp/package.json +++ b/engines/minizinc-csp/package.json @@ -1,6 +1,7 @@ { "name": "minizinc-csp-engine", "version": "0.1.0", + "packageManager": "pnpm@10.12.4", "description": "MiniZinc CSP Solver Service for OpenBinding", "main": "dist/index.js", "scripts": { @@ -18,4 +19,4 @@ "@types/node": "^20.11.19", "ts-node": "^10.9.2" } -} \ No newline at end of file +} diff --git a/openbinding-gateway/src/openbinding_gateway/registry/engine.py b/openbinding-gateway/src/openbinding_gateway/registry/engine.py index a269738..2f08298 100644 --- a/openbinding-gateway/src/openbinding_gateway/registry/engine.py +++ b/openbinding-gateway/src/openbinding_gateway/registry/engine.py @@ -4,6 +4,7 @@ from ..validation.engine_plugins.minizinc_csp import MiniZincCSPEnginePlugin from ..validation.engine_plugins.random_search import RandomSearchEnginePlugin from ..validation.engine_plugins.many_heuristic import ManyHeuristicEnginePlugin +from ..validation.engine_plugins.evolutionary_heuristics import EvolutionaryHeuristicsEnginePlugin class EngineRegistry: # Keeps track of all solver engines and where to find them. @@ -11,7 +12,11 @@ class EngineRegistry: _engine_urls: Dict[str, str] = { "minizinc-csp": os.getenv("ENGINE_MINIZINC_URL", "http://engine-minizinc:3000"), "random-search": os.getenv("ENGINE_RANDOM_SEARCH_URL", "http://engine-random-search:8080"), - "many-heuristic": os.getenv("ENGINE_MANY_HEURISTIC_URL", "http://engine-many-heuristic:8080") + "many-heuristic": os.getenv("ENGINE_MANY_HEURISTIC_URL", "http://engine-many-heuristic:8080"), + "evolutionary-heuristics": os.getenv( + "ENGINE_EVOLUTIONARY_HEURISTICS_URL", + "http://engine-evolutionary-heuristics:8080", + ), } @classmethod @@ -44,3 +49,4 @@ def list_engines(cls) -> List[Dict[str, Any]]: EngineRegistry.register("minizinc-csp", MiniZincCSPEnginePlugin()) EngineRegistry.register("random-search", RandomSearchEnginePlugin()) EngineRegistry.register("many-heuristic", ManyHeuristicEnginePlugin()) +EngineRegistry.register("evolutionary-heuristics", EvolutionaryHeuristicsEnginePlugin()) diff --git a/openbinding-gateway/src/openbinding_gateway/validation/engine_plugins/evolutionary_heuristics.py b/openbinding-gateway/src/openbinding_gateway/validation/engine_plugins/evolutionary_heuristics.py new file mode 100644 index 0000000..31b442c --- /dev/null +++ b/openbinding-gateway/src/openbinding_gateway/validation/engine_plugins/evolutionary_heuristics.py @@ -0,0 +1,124 @@ +import os +from typing import Any, Dict, List, Tuple + +import httpx + +from .base import EngineValidationPlugin +from ...models.api import ValidationViolation + + +class EvolutionaryHeuristicsEnginePlugin(EngineValidationPlugin): + _VALID_OPTIONS = { + "algorithm", + "population_size", + "max_evaluations", + "crossover_probability", + "mutation_probability", + "distribution_index", + "archive_size", + "soft_penalty", + "seed", + "reference_divisions", + } + + async def check_engine_health(self, base_url: str, client: httpx.AsyncClient) -> bool: + try: + response = await client.get(f"{base_url.rstrip('/')}/health") + return response.status_code == 200 + except Exception: + return False + + def get_capabilities(self) -> Dict[str, Any]: + return { + "qos_features_supported": ["*"], + "composition_nodes_supported": ["TASK", "SEQ", "AND", "XOR", "LOOP"], + "objective_types_supported": ["MONO", "MULTI", "MANY"], + "constraints_supported": ["attribute_bound", "dependency"], + "algorithms_supported": ["NSGAII", "NSGAIII"], + "type": "HEURISTIC", + "schema_version": "v1", + } + + def get_default_options(self) -> Dict[str, Any]: + return { + "algorithm": "AUTO", + "population_size": 100, + "max_evaluations": 10000, + "crossover_probability": 0.9, + "mutation_probability": None, + "distribution_index": 20.0, + "archive_size": 100, + "soft_penalty": 10.0, + "seed": 1, + "reference_divisions": 12, + } + + def get_specialization_schema_path(self) -> str: + base_path = os.getenv("SCHEMAS_DIR", "/app/schemas") + if not os.path.exists(base_path): + base_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), "../../../../../../schemas") + ) + return os.path.join( + base_path, "specializations/evolutionary-heuristics.schema.json" + ) + + def validate_semantics(self, instance: Dict[str, Any]) -> List[ValidationViolation]: + violations: List[ValidationViolation] = [] + task_ids = set() + + def visit(node: Dict[str, Any]) -> None: + kind = node.get("kind") + if kind == "TASK": + task_ids.add(node.get("task_id")) + for child in node.get("children", []) or []: + visit(child) + for branch in node.get("branches", []) or []: + visit(branch.get("child", {})) + if node.get("body"): + visit(node["body"]) + + visit(instance["composition"]["root"]) + candidate_tasks = { + candidate.get("task_id") for candidate in instance.get("candidates", []) + } + for task_id in sorted(task_ids - candidate_tasks): + violations.append( + ValidationViolation( + code="missing_candidates", + path="candidates", + message=f"Missing candidates for task '{task_id}'", + ) + ) + + features = {feature["id"] for feature in instance.get("features", [])} + for target in instance.get("objective", {}).get("targets", []): + if target not in features: + violations.append( + ValidationViolation( + code="unknown_objective_target", + path="objective.targets", + message=f"Unknown objective feature '{target}'", + ) + ) + return violations + + def transform_request( + self, instance: Dict[str, Any], options: Dict[str, Any] = {} + ) -> Tuple[Dict[str, Any], List[str]]: + warnings = [ + f"Option '{name}' is not supported by evolutionary-heuristics" + for name in options + if name not in self._VALID_OPTIONS + ] + filtered_options = { + name: value + for name, value in options.items() + if name in self._VALID_OPTIONS and value is not None + } + return {"instance": instance, "options": filtered_options}, warnings + + def transform_response( + self, engine_response: Dict[str, Any], original_request: Dict[str, Any] + ) -> Dict[str, Any]: + return engine_response diff --git a/openbinding-gateway/tests/test_evolutionary_heuristics_plugin.py b/openbinding-gateway/tests/test_evolutionary_heuristics_plugin.py new file mode 100644 index 0000000..44ac301 --- /dev/null +++ b/openbinding-gateway/tests/test_evolutionary_heuristics_plugin.py @@ -0,0 +1,45 @@ +from openbinding_gateway.validation.engine_plugins.evolutionary_heuristics import ( + EvolutionaryHeuristicsEnginePlugin, +) + + +def test_evolutionary_plugin_passes_general_instance_and_filters_options(): + plugin = EvolutionaryHeuristicsEnginePlugin() + instance = { + "composition": {"root": {"kind": "TASK", "task_id": "t1"}}, + "features": [{"id": "cost"}], + "candidates": [{"id": "c1", "task_id": "t1"}], + "objective": {"type": "MONO", "targets": ["cost"]}, + } + + payload, warnings = plugin.transform_request( + instance, + {"population_size": 50, "seed": 7, "unsupported": True}, + ) + + assert payload["instance"] is instance + assert payload["options"] == {"population_size": 50, "seed": 7} + assert warnings == [ + "Option 'unsupported' is not supported by evolutionary-heuristics" + ] + + +def test_evolutionary_plugin_reports_missing_candidates(): + plugin = EvolutionaryHeuristicsEnginePlugin() + instance = { + "composition": {"root": {"kind": "TASK", "task_id": "t1"}}, + "features": [{"id": "cost"}], + "candidates": [], + "objective": {"type": "MONO", "targets": ["cost"]}, + } + + violations = plugin.validate_semantics(instance) + + assert [violation.code for violation in violations] == ["missing_candidates"] + + +def test_evolutionary_plugin_capabilities_cover_all_objective_types(): + capabilities = EvolutionaryHeuristicsEnginePlugin().get_capabilities() + + assert capabilities["objective_types_supported"] == ["MONO", "MULTI", "MANY"] + assert capabilities["algorithms_supported"] == ["NSGAII", "NSGAIII"] diff --git a/schemas/specializations/evolutionary-heuristics.schema.json b/schemas/specializations/evolutionary-heuristics.schema.json new file mode 100644 index 0000000..e11f9c4 --- /dev/null +++ b/schemas/specializations/evolutionary-heuristics.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openbinding.score.us.es/api/v1/schemas/evolutionary-heuristics", + "title": "Evolutionary Heuristics Specialization", + "description": "Profile for the Java 21/jMetal evolutionary engine.", + "type": "object", + "properties": { + "composition": { + "type": "object", + "properties": { + "type": { "const": "STRUCTURED" } + }, + "required": ["type", "root"] + }, + "objective": { + "type": "object", + "properties": { + "type": { "enum": ["MONO", "MULTI", "MANY"] } + }, + "required": ["type", "targets", "weights"] + }, + "constraints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { "enum": ["ATTRIBUTE_BOUND", "DEPENDENCY"] } + }, + "required": ["kind"] + } + } + }, + "required": [ + "composition", + "features", + "candidates", + "aggregation_policies", + "objective" + ] +} diff --git a/schemas/specializations/evolutionary-heuristics.schema.mermaid b/schemas/specializations/evolutionary-heuristics.schema.mermaid new file mode 100644 index 0000000..1f07cf6 --- /dev/null +++ b/schemas/specializations/evolutionary-heuristics.schema.mermaid @@ -0,0 +1,28 @@ +classDiagram + class EvolutionaryBindingProblem { + +STRUCTURED composition + +MONO|MULTI|MANY objective + +ATTRIBUTE_BOUND constraints + +DEPENDENCY constraints + } + class IntegerChromosome { + +one gene per task + +candidate index allele + } + class QualityEvaluator { + +aggregate composition QoS + +normalize global quality + +compute objective losses + } + class ConstraintEvaluator { + +hard feasibility distance + +soft violation distance + } + class NSGAII + class NSGAIII + + EvolutionaryBindingProblem --> IntegerChromosome + EvolutionaryBindingProblem --> QualityEvaluator + EvolutionaryBindingProblem --> ConstraintEvaluator + EvolutionaryBindingProblem --> NSGAII : MONO / MULTI + EvolutionaryBindingProblem --> NSGAIII : MANY