diff --git a/.github/workflows/release-changelog-template.md b/.github/workflows/release-changelog-template.md index 82d4904486c..7f0632c26f8 100644 --- a/.github/workflows/release-changelog-template.md +++ b/.github/workflows/release-changelog-template.md @@ -14,7 +14,7 @@ For more, see [Contributing](https://github.com/TimefoldAI/timefold-solver/blob/ Should your business need to scale to truly massive data sets or require enterprise-grade support, check out [_Timefold Solver Enterprise Edition_](https://docs.timefold.ai/timefold-solver/latest/enterprise-edition/enterprise-edition). -Timefold Solver Enterprise Edition requires [a license](https://timefold.ai/pricing). +Enterprise Edition requires [a license](https://timefold.ai/pricing). # How to use Timefold Solver diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh deleted file mode 100755 index 98e387c2717..00000000000 --- a/.specify/scripts/bash/check-prerequisites.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env bash - -# Consolidated prerequisite checking script -# -# This script provides unified prerequisite checking for Spec-Driven Development workflow. -# It replaces the functionality previously spread across multiple scripts. -# -# Usage: ./check-prerequisites.sh [OPTIONS] -# -# OPTIONS: -# --json Output in JSON format -# --require-tasks Require tasks.md to exist (for implementation phase) -# --include-tasks Include tasks.md in AVAILABLE_DOCS list -# --paths-only Only output path variables (no validation) -# --help, -h Show help message -# -# OUTPUTS: -# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} -# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md -# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. - -set -e - -# Parse command line arguments -JSON_MODE=false -REQUIRE_TASKS=false -INCLUDE_TASKS=false -PATHS_ONLY=false - -for arg in "$@"; do - case "$arg" in - --json) - JSON_MODE=true - ;; - --require-tasks) - REQUIRE_TASKS=true - ;; - --include-tasks) - INCLUDE_TASKS=true - ;; - --paths-only) - PATHS_ONLY=true - ;; - --help|-h) - cat << 'EOF' -Usage: check-prerequisites.sh [OPTIONS] - -Consolidated prerequisite checking for Spec-Driven Development workflow. - -OPTIONS: - --json Output in JSON format - --require-tasks Require tasks.md to exist (for implementation phase) - --include-tasks Include tasks.md in AVAILABLE_DOCS list - --paths-only Only output path variables (no prerequisite validation) - --help, -h Show this help message - -EXAMPLES: - # Check task prerequisites (plan.md required) - ./check-prerequisites.sh --json - - # Check implementation prerequisites (plan.md + tasks.md required) - ./check-prerequisites.sh --json --require-tasks --include-tasks - - # Get feature paths only (no validation) - ./check-prerequisites.sh --paths-only - -EOF - exit 0 - ;; - *) - echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 - exit 1 - ;; - esac -done - -# Source common functions -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get feature paths and validate branch -eval $(get_feature_paths) -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 - -# If paths-only mode, output paths and exit (support JSON + paths-only combined) -if $PATHS_ONLY; then - if $JSON_MODE; then - # Minimal JSON paths payload (no validation performed) - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" - else - echo "REPO_ROOT: $REPO_ROOT" - echo "BRANCH: $CURRENT_BRANCH" - echo "FEATURE_DIR: $FEATURE_DIR" - echo "FEATURE_SPEC: $FEATURE_SPEC" - echo "IMPL_PLAN: $IMPL_PLAN" - echo "TASKS: $TASKS" - fi - exit 0 -fi - -# Validate required directories and files -if [[ ! -d "$FEATURE_DIR" ]]; then - echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 - echo "Run /speckit.specify first to create the feature structure." >&2 - exit 1 -fi - -if [[ ! -f "$IMPL_PLAN" ]]; then - echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 - echo "Run /speckit.plan first to create the implementation plan." >&2 - exit 1 -fi - -# Check for tasks.md if required -if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then - echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 - echo "Run /speckit.tasks first to create the task list." >&2 - exit 1 -fi - -# Build list of available documents -docs=() - -# Always check these optional docs -[[ -f "$RESEARCH" ]] && docs+=("research.md") -[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") - -# Check contracts directory (only if it exists and has files) -if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then - docs+=("contracts/") -fi - -[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") - -# Include tasks.md if requested and it exists -if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then - docs+=("tasks.md") -fi - -# Output results -if $JSON_MODE; then - # Build JSON array of documents - if [[ ${#docs[@]} -eq 0 ]]; then - json_docs="[]" - else - json_docs=$(printf '"%s",' "${docs[@]}") - json_docs="[${json_docs%,}]" - fi - - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" -else - # Text output - echo "FEATURE_DIR:$FEATURE_DIR" - echo "AVAILABLE_DOCS:" - - # Show status of each potential document - check_file "$RESEARCH" "research.md" - check_file "$DATA_MODEL" "data-model.md" - check_dir "$CONTRACTS_DIR" "contracts/" - check_file "$QUICKSTART" "quickstart.md" - - if $INCLUDE_TASKS; then - check_file "$TASKS" "tasks.md" - fi -fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh deleted file mode 100755 index 2c3165e41d8..00000000000 --- a/.specify/scripts/bash/common.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash -# Common functions and variables for all scripts - -# Get repository root, with fallback for non-git repositories -get_repo_root() { - if git rev-parse --show-toplevel >/dev/null 2>&1; then - git rev-parse --show-toplevel - else - # Fall back to script location for non-git repos - local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - (cd "$script_dir/../../.." && pwd) - fi -} - -# Get current branch, with fallback for non-git repositories -get_current_branch() { - # First check if SPECIFY_FEATURE environment variable is set - if [[ -n "${SPECIFY_FEATURE:-}" ]]; then - echo "$SPECIFY_FEATURE" - return - fi - - # Then check git if available - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then - git rev-parse --abbrev-ref HEAD - return - fi - - # For non-git repos, try to find the latest feature directory - local repo_root=$(get_repo_root) - local specs_dir="$repo_root/specs" - - if [[ -d "$specs_dir" ]]; then - local latest_feature="" - local highest=0 - - for dir in "$specs_dir"/*; do - if [[ -d "$dir" ]]; then - local dirname=$(basename "$dir") - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then - local number=${BASH_REMATCH[1]} - number=$((10#$number)) - if [[ "$number" -gt "$highest" ]]; then - highest=$number - latest_feature=$dirname - fi - fi - fi - done - - if [[ -n "$latest_feature" ]]; then - echo "$latest_feature" - return - fi - fi - - echo "main" # Final fallback -} - -# Check if we have git available -has_git() { - git rev-parse --show-toplevel >/dev/null 2>&1 -} - -check_feature_branch() { - local branch="$1" - local has_git_repo="$2" - - # For non-git repos, we can't enforce branch naming but still provide output - if [[ "$has_git_repo" != "true" ]]; then - echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 - return 0 - fi - - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name" >&2 - return 1 - fi - - return 0 -} - -get_feature_dir() { echo "$1/specs/$2"; } - -# Find feature directory by numeric prefix instead of exact branch match -# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) -find_feature_dir_by_prefix() { - local repo_root="$1" - local branch_name="$2" - local specs_dir="$repo_root/specs" - - # Extract numeric prefix from branch (e.g., "004" from "004-whatever") - if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then - # If branch doesn't have numeric prefix, fall back to exact match - echo "$specs_dir/$branch_name" - return - fi - - local prefix="${BASH_REMATCH[1]}" - - # Search for directories in specs/ that start with this prefix - local matches=() - if [[ -d "$specs_dir" ]]; then - for dir in "$specs_dir"/"$prefix"-*; do - if [[ -d "$dir" ]]; then - matches+=("$(basename "$dir")") - fi - done - fi - - # Handle results - if [[ ${#matches[@]} -eq 0 ]]; then - # No match found - return the branch name path (will fail later with clear error) - echo "$specs_dir/$branch_name" - elif [[ ${#matches[@]} -eq 1 ]]; then - # Exactly one match - perfect! - echo "$specs_dir/${matches[0]}" - else - # Multiple matches - this shouldn't happen with proper naming convention - echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 - echo "Please ensure only one spec directory exists per numeric prefix." >&2 - echo "$specs_dir/$branch_name" # Return something to avoid breaking the script - fi -} - -get_feature_paths() { - local repo_root=$(get_repo_root) - local current_branch=$(get_current_branch) - local has_git_repo="false" - - if has_git; then - has_git_repo="true" - fi - - # Use prefix-based lookup to support multiple branches per spec - local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") - - cat </dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } - diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh deleted file mode 100755 index c40cfd77f05..00000000000 --- a/.specify/scripts/bash/create-new-feature.sh +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env bash - -set -e - -JSON_MODE=false -SHORT_NAME="" -BRANCH_NUMBER="" -ARGS=() -i=1 -while [ $i -le $# ]; do - arg="${!i}" - case "$arg" in - --json) - JSON_MODE=true - ;; - --short-name) - if [ $((i + 1)) -gt $# ]; then - echo 'Error: --short-name requires a value' >&2 - exit 1 - fi - i=$((i + 1)) - next_arg="${!i}" - # Check if the next argument is another option (starts with --) - if [[ "$next_arg" == --* ]]; then - echo 'Error: --short-name requires a value' >&2 - exit 1 - fi - SHORT_NAME="$next_arg" - ;; - --number) - if [ $((i + 1)) -gt $# ]; then - echo 'Error: --number requires a value' >&2 - exit 1 - fi - i=$((i + 1)) - next_arg="${!i}" - if [[ "$next_arg" == --* ]]; then - echo 'Error: --number requires a value' >&2 - exit 1 - fi - BRANCH_NUMBER="$next_arg" - ;; - --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] " - echo "" - echo "Options:" - echo " --json Output in JSON format" - echo " --short-name Provide a custom short name (2-4 words) for the branch" - echo " --number N Specify branch number manually (overrides auto-detection)" - echo " --help, -h Show this help message" - echo "" - echo "Examples:" - echo " $0 'Add user authentication system' --short-name 'user-auth'" - echo " $0 'Implement OAuth2 integration for API' --number 5" - exit 0 - ;; - *) - ARGS+=("$arg") - ;; - esac - i=$((i + 1)) -done - -FEATURE_DESCRIPTION="${ARGS[*]}" -if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] [--short-name ] [--number N] " >&2 - exit 1 -fi - -# Function to find the repository root by searching for existing project markers -find_repo_root() { - local dir="$1" - while [ "$dir" != "/" ]; do - if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then - echo "$dir" - return 0 - fi - dir="$(dirname "$dir")" - done - return 1 -} - -# Function to get highest number from specs directory -get_highest_from_specs() { - local specs_dir="$1" - local highest=0 - - if [ -d "$specs_dir" ]; then - for dir in "$specs_dir"/*; do - [ -d "$dir" ] || continue - dirname=$(basename "$dir") - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") - number=$((10#$number)) - if [ "$number" -gt "$highest" ]; then - highest=$number - fi - done - fi - - echo "$highest" -} - -# Function to get highest number from git branches -get_highest_from_branches() { - local highest=0 - - # Get all branches (local and remote) - branches=$(git branch -a 2>/dev/null || echo "") - - if [ -n "$branches" ]; then - while IFS= read -r branch; do - # Clean branch name: remove leading markers and remote prefixes - clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') - - # Extract feature number if branch matches pattern ###-* - if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then - number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") - number=$((10#$number)) - if [ "$number" -gt "$highest" ]; then - highest=$number - fi - fi - done <<< "$branches" - fi - - echo "$highest" -} - -# Function to check existing branches (local and remote) and return next available number -check_existing_branches() { - local specs_dir="$1" - - # Fetch all remotes to get latest branch info (suppress errors if no remotes) - git fetch --all --prune 2>/dev/null || true - - # Get highest number from ALL branches (not just matching short name) - local highest_branch=$(get_highest_from_branches) - - # Get highest number from ALL specs (not just matching short name) - local highest_spec=$(get_highest_from_specs "$specs_dir") - - # Take the maximum of both - local max_num=$highest_branch - if [ "$highest_spec" -gt "$max_num" ]; then - max_num=$highest_spec - fi - - # Return next number - echo $((max_num + 1)) -} - -# Function to clean and format a branch name -clean_branch_name() { - local name="$1" - echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' -} - -# Resolve repository root. Prefer git information when available, but fall back -# to searching for repository markers so the workflow still functions in repositories that -# were initialised with --no-git. -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -if git rev-parse --show-toplevel >/dev/null 2>&1; then - REPO_ROOT=$(git rev-parse --show-toplevel) - HAS_GIT=true -else - REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" - if [ -z "$REPO_ROOT" ]; then - echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 - exit 1 - fi - HAS_GIT=false -fi - -cd "$REPO_ROOT" - -SPECS_DIR="$REPO_ROOT/specs" -mkdir -p "$SPECS_DIR" - -# Function to generate branch name with stop word filtering and length filtering -generate_branch_name() { - local description="$1" - - # Common stop words to filter out - local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" - - # Convert to lowercase and split into words - local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') - - # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) - local meaningful_words=() - for word in $clean_name; do - # Skip empty words - [ -z "$word" ] && continue - - # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) - if ! echo "$word" | grep -qiE "$stop_words"; then - if [ ${#word} -ge 3 ]; then - meaningful_words+=("$word") - elif echo "$description" | grep -q "\b${word^^}\b"; then - # Keep short words if they appear as uppercase in original (likely acronyms) - meaningful_words+=("$word") - fi - fi - done - - # If we have meaningful words, use first 3-4 of them - if [ ${#meaningful_words[@]} -gt 0 ]; then - local max_words=3 - if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi - - local result="" - local count=0 - for word in "${meaningful_words[@]}"; do - if [ $count -ge $max_words ]; then break; fi - if [ -n "$result" ]; then result="$result-"; fi - result="$result$word" - count=$((count + 1)) - done - echo "$result" - else - # Fallback to original logic if no meaningful words found - local cleaned=$(clean_branch_name "$description") - echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' - fi -} - -# Generate branch name -if [ -n "$SHORT_NAME" ]; then - # Use provided short name, just clean it up - BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") -else - # Generate from description with smart filtering - BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") -fi - -# Determine branch number -if [ -z "$BRANCH_NUMBER" ]; then - if [ "$HAS_GIT" = true ]; then - # Check existing branches on remotes - BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") - else - # Fall back to local directory check - HIGHEST=$(get_highest_from_specs "$SPECS_DIR") - BRANCH_NUMBER=$((HIGHEST + 1)) - fi -fi - -# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) -FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" - -# GitHub enforces a 244-byte limit on branch names -# Validate and truncate if necessary -MAX_BRANCH_LENGTH=244 -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then - # Calculate how much we need to trim from suffix - # Account for: feature number (3) + hyphen (1) = 4 chars - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) - - # Truncate suffix at word boundary if possible - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) - # Remove trailing hyphen if truncation created one - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') - - ORIGINAL_BRANCH_NAME="$BRANCH_NAME" - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" - - >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" - >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" - >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" -fi - -if [ "$HAS_GIT" = true ]; then - git checkout -b "$BRANCH_NAME" -else - >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" -fi - -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" -mkdir -p "$FEATURE_DIR" - -TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" -SPEC_FILE="$FEATURE_DIR/spec.md" -if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi - -# Set the SPECIFY_FEATURE environment variable for the current session -export SPECIFY_FEATURE="$BRANCH_NAME" - -if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" -else - echo "BRANCH_NAME: $BRANCH_NAME" - echo "SPEC_FILE: $SPEC_FILE" - echo "FEATURE_NUM: $FEATURE_NUM" - echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" -fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh deleted file mode 100755 index d01c6d6cb56..00000000000 --- a/.specify/scripts/bash/setup-plan.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Parse command line arguments -JSON_MODE=false -ARGS=() - -for arg in "$@"; do - case "$arg" in - --json) - JSON_MODE=true - ;; - --help|-h) - echo "Usage: $0 [--json]" - echo " --json Output results in JSON format" - echo " --help Show this help message" - exit 0 - ;; - *) - ARGS+=("$arg") - ;; - esac -done - -# Get script directory and load common functions -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths and variables from common functions -eval $(get_feature_paths) - -# Check if we're on a proper feature branch (only for git repos) -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 - -# Ensure the feature directory exists -mkdir -p "$FEATURE_DIR" - -# Copy plan template if it exists -TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" -if [[ -f "$TEMPLATE" ]]; then - cp "$TEMPLATE" "$IMPL_PLAN" - echo "Copied plan template to $IMPL_PLAN" -else - echo "Warning: Plan template not found at $TEMPLATE" - # Create a basic plan file if template doesn't exist - touch "$IMPL_PLAN" -fi - -# Output results -if $JSON_MODE; then - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" -else - echo "FEATURE_SPEC: $FEATURE_SPEC" - echo "IMPL_PLAN: $IMPL_PLAN" - echo "SPECS_DIR: $FEATURE_DIR" - echo "BRANCH: $CURRENT_BRANCH" - echo "HAS_GIT: $HAS_GIT" -fi - diff --git a/.specify/scripts/bash/update-agent-context.sh b/.specify/scripts/bash/update-agent-context.sh deleted file mode 100755 index 6d3e0b37ab8..00000000000 --- a/.specify/scripts/bash/update-agent-context.sh +++ /dev/null @@ -1,799 +0,0 @@ -#!/usr/bin/env bash - -# Update agent context files with information from plan.md -# -# This script maintains AI agent context files by parsing feature specifications -# and updating agent-specific configuration files with project information. -# -# MAIN FUNCTIONS: -# 1. Environment Validation -# - Verifies git repository structure and branch information -# - Checks for required plan.md files and templates -# - Validates file permissions and accessibility -# -# 2. Plan Data Extraction -# - Parses plan.md files to extract project metadata -# - Identifies language/version, frameworks, databases, and project types -# - Handles missing or incomplete specification data gracefully -# -# 3. Agent File Management -# - Creates new agent context files from templates when needed -# - Updates existing agent files with new project information -# - Preserves manual additions and custom configurations -# - Supports multiple AI agent formats and directory structures -# -# 4. Content Generation -# - Generates language-specific build/test commands -# - Creates appropriate project directory structures -# - Updates technology stacks and recent changes sections -# - Maintains consistent formatting and timestamps -# -# 5. Multi-Agent Support -# - Handles agent-specific file paths and naming conventions -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, or Amazon Q Developer CLI -# - Can update single agents or all existing agent files -# - Creates default Claude file if no agent files exist -# -# Usage: ./update-agent-context.sh [agent_type] -# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|shai|q|bob|qoder -# Leave empty to update all existing agent files - -set -e - -# Enable strict error handling -set -u -set -o pipefail - -#============================================================================== -# Configuration and Global Variables -#============================================================================== - -# Get script directory and load common functions -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths and variables from common functions -eval $(get_feature_paths) - -NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code -AGENT_TYPE="${1:-}" - -# Agent-specific file paths -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" -GEMINI_FILE="$REPO_ROOT/GEMINI.md" -COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" -CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" -QWEN_FILE="$REPO_ROOT/QWEN.md" -AGENTS_FILE="$REPO_ROOT/AGENTS.md" -WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" -KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" -AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" -ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" -CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" -QODER_FILE="$REPO_ROOT/QODER.md" -AMP_FILE="$REPO_ROOT/AGENTS.md" -SHAI_FILE="$REPO_ROOT/SHAI.md" -Q_FILE="$REPO_ROOT/AGENTS.md" -BOB_FILE="$REPO_ROOT/AGENTS.md" - -# Template file -TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" - -# Global variables for parsed plan data -NEW_LANG="" -NEW_FRAMEWORK="" -NEW_DB="" -NEW_PROJECT_TYPE="" - -#============================================================================== -# Utility Functions -#============================================================================== - -log_info() { - echo "INFO: $1" -} - -log_success() { - echo "✓ $1" -} - -log_error() { - echo "ERROR: $1" >&2 -} - -log_warning() { - echo "WARNING: $1" >&2 -} - -# Cleanup function for temporary files -cleanup() { - local exit_code=$? - rm -f /tmp/agent_update_*_$$ - rm -f /tmp/manual_additions_$$ - exit $exit_code -} - -# Set up cleanup trap -trap cleanup EXIT INT TERM - -#============================================================================== -# Validation Functions -#============================================================================== - -validate_environment() { - # Check if we have a current branch/feature (git or non-git) - if [[ -z "$CURRENT_BRANCH" ]]; then - log_error "Unable to determine current feature" - if [[ "$HAS_GIT" == "true" ]]; then - log_info "Make sure you're on a feature branch" - else - log_info "Set SPECIFY_FEATURE environment variable or create a feature first" - fi - exit 1 - fi - - # Check if plan.md exists - if [[ ! -f "$NEW_PLAN" ]]; then - log_error "No plan.md found at $NEW_PLAN" - log_info "Make sure you're working on a feature with a corresponding spec directory" - if [[ "$HAS_GIT" != "true" ]]; then - log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" - fi - exit 1 - fi - - # Check if template exists (needed for new files) - if [[ ! -f "$TEMPLATE_FILE" ]]; then - log_warning "Template file not found at $TEMPLATE_FILE" - log_warning "Creating new agent files will fail" - fi -} - -#============================================================================== -# Plan Parsing Functions -#============================================================================== - -extract_plan_field() { - local field_pattern="$1" - local plan_file="$2" - - grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ - head -1 | \ - sed "s|^\*\*${field_pattern}\*\*: ||" | \ - sed 's/^[ \t]*//;s/[ \t]*$//' | \ - grep -v "NEEDS CLARIFICATION" | \ - grep -v "^N/A$" || echo "" -} - -parse_plan_data() { - local plan_file="$1" - - if [[ ! -f "$plan_file" ]]; then - log_error "Plan file not found: $plan_file" - return 1 - fi - - if [[ ! -r "$plan_file" ]]; then - log_error "Plan file is not readable: $plan_file" - return 1 - fi - - log_info "Parsing plan data from $plan_file" - - NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") - NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") - NEW_DB=$(extract_plan_field "Storage" "$plan_file") - NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") - - # Log what we found - if [[ -n "$NEW_LANG" ]]; then - log_info "Found language: $NEW_LANG" - else - log_warning "No language information found in plan" - fi - - if [[ -n "$NEW_FRAMEWORK" ]]; then - log_info "Found framework: $NEW_FRAMEWORK" - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then - log_info "Found database: $NEW_DB" - fi - - if [[ -n "$NEW_PROJECT_TYPE" ]]; then - log_info "Found project type: $NEW_PROJECT_TYPE" - fi -} - -format_technology_stack() { - local lang="$1" - local framework="$2" - local parts=() - - # Add non-empty parts - [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") - [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") - - # Join with proper formatting - if [[ ${#parts[@]} -eq 0 ]]; then - echo "" - elif [[ ${#parts[@]} -eq 1 ]]; then - echo "${parts[0]}" - else - # Join multiple parts with " + " - local result="${parts[0]}" - for ((i=1; i<${#parts[@]}; i++)); do - result="$result + ${parts[i]}" - done - echo "$result" - fi -} - -#============================================================================== -# Template and Content Generation Functions -#============================================================================== - -get_project_structure() { - local project_type="$1" - - if [[ "$project_type" == *"web"* ]]; then - echo "backend/\\nfrontend/\\ntests/" - else - echo "src/\\ntests/" - fi -} - -get_commands_for_language() { - local lang="$1" - - case "$lang" in - *"Python"*) - echo "cd src && pytest && ruff check ." - ;; - *"Rust"*) - echo "cargo test && cargo clippy" - ;; - *"JavaScript"*|*"TypeScript"*) - echo "npm test \\&\\& npm run lint" - ;; - *) - echo "# Add commands for $lang" - ;; - esac -} - -get_language_conventions() { - local lang="$1" - echo "$lang: Follow standard conventions" -} - -create_new_agent_file() { - local target_file="$1" - local temp_file="$2" - local project_name="$3" - local current_date="$4" - - if [[ ! -f "$TEMPLATE_FILE" ]]; then - log_error "Template not found at $TEMPLATE_FILE" - return 1 - fi - - if [[ ! -r "$TEMPLATE_FILE" ]]; then - log_error "Template file is not readable: $TEMPLATE_FILE" - return 1 - fi - - log_info "Creating new agent context file from template..." - - if ! cp "$TEMPLATE_FILE" "$temp_file"; then - log_error "Failed to copy template file" - return 1 - fi - - # Replace template placeholders - local project_structure - project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") - - local commands - commands=$(get_commands_for_language "$NEW_LANG") - - local language_conventions - language_conventions=$(get_language_conventions "$NEW_LANG") - - # Perform substitutions with error checking using safer approach - # Escape special characters for sed by using a different delimiter or escaping - local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') - local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') - local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') - - # Build technology stack and recent change strings conditionally - local tech_stack - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then - tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" - elif [[ -n "$escaped_lang" ]]; then - tech_stack="- $escaped_lang ($escaped_branch)" - elif [[ -n "$escaped_framework" ]]; then - tech_stack="- $escaped_framework ($escaped_branch)" - else - tech_stack="- ($escaped_branch)" - fi - - local recent_change - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then - recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" - elif [[ -n "$escaped_lang" ]]; then - recent_change="- $escaped_branch: Added $escaped_lang" - elif [[ -n "$escaped_framework" ]]; then - recent_change="- $escaped_branch: Added $escaped_framework" - else - recent_change="- $escaped_branch: Added" - fi - - local substitutions=( - "s|\[PROJECT NAME\]|$project_name|" - "s|\[DATE\]|$current_date|" - "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" - "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" - "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" - "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" - "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" - ) - - for substitution in "${substitutions[@]}"; do - if ! sed -i.bak -e "$substitution" "$temp_file"; then - log_error "Failed to perform substitution: $substitution" - rm -f "$temp_file" "$temp_file.bak" - return 1 - fi - done - - # Convert \n sequences to actual newlines - newline=$(printf '\n') - sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" - - # Clean up backup files - rm -f "$temp_file.bak" "$temp_file.bak2" - - return 0 -} - - - - -update_existing_agent_file() { - local target_file="$1" - local current_date="$2" - - log_info "Updating existing agent context file..." - - # Use a single temporary file for atomic update - local temp_file - temp_file=$(mktemp) || { - log_error "Failed to create temporary file" - return 1 - } - - # Process the file in one pass - local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") - local new_tech_entries=() - local new_change_entry="" - - # Prepare new technology entries - if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then - new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then - new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") - fi - - # Prepare new change entry - if [[ -n "$tech_stack" ]]; then - new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" - elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then - new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" - fi - - # Check if sections exist in the file - local has_active_technologies=0 - local has_recent_changes=0 - - if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then - has_active_technologies=1 - fi - - if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then - has_recent_changes=1 - fi - - # Process file line by line - local in_tech_section=false - local in_changes_section=false - local tech_entries_added=false - local changes_entries_added=false - local existing_changes_count=0 - local file_ended=false - - while IFS= read -r line || [[ -n "$line" ]]; do - # Handle Active Technologies section - if [[ "$line" == "## Active Technologies" ]]; then - echo "$line" >> "$temp_file" - in_tech_section=true - continue - elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then - # Add new tech entries before closing the section - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - echo "$line" >> "$temp_file" - in_tech_section=false - continue - elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then - # Add new tech entries before empty line in tech section - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - echo "$line" >> "$temp_file" - continue - fi - - # Handle Recent Changes section - if [[ "$line" == "## Recent Changes" ]]; then - echo "$line" >> "$temp_file" - # Add new change entry right after the heading - if [[ -n "$new_change_entry" ]]; then - echo "$new_change_entry" >> "$temp_file" - fi - in_changes_section=true - changes_entries_added=true - continue - elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then - echo "$line" >> "$temp_file" - in_changes_section=false - continue - elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then - # Keep only first 2 existing changes - if [[ $existing_changes_count -lt 2 ]]; then - echo "$line" >> "$temp_file" - ((existing_changes_count++)) - fi - continue - fi - - # Update timestamp - if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then - echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" - else - echo "$line" >> "$temp_file" - fi - done < "$target_file" - - # Post-loop check: if we're still in the Active Technologies section and haven't added new entries - if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - - # If sections don't exist, add them at the end of the file - if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - echo "" >> "$temp_file" - echo "## Active Technologies" >> "$temp_file" - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - - if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then - echo "" >> "$temp_file" - echo "## Recent Changes" >> "$temp_file" - echo "$new_change_entry" >> "$temp_file" - changes_entries_added=true - fi - - # Move temp file to target atomically - if ! mv "$temp_file" "$target_file"; then - log_error "Failed to update target file" - rm -f "$temp_file" - return 1 - fi - - return 0 -} -#============================================================================== -# Main Agent File Update Function -#============================================================================== - -update_agent_file() { - local target_file="$1" - local agent_name="$2" - - if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then - log_error "update_agent_file requires target_file and agent_name parameters" - return 1 - fi - - log_info "Updating $agent_name context file: $target_file" - - local project_name - project_name=$(basename "$REPO_ROOT") - local current_date - current_date=$(date +%Y-%m-%d) - - # Create directory if it doesn't exist - local target_dir - target_dir=$(dirname "$target_file") - if [[ ! -d "$target_dir" ]]; then - if ! mkdir -p "$target_dir"; then - log_error "Failed to create directory: $target_dir" - return 1 - fi - fi - - if [[ ! -f "$target_file" ]]; then - # Create new file from template - local temp_file - temp_file=$(mktemp) || { - log_error "Failed to create temporary file" - return 1 - } - - if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then - if mv "$temp_file" "$target_file"; then - log_success "Created new $agent_name context file" - else - log_error "Failed to move temporary file to $target_file" - rm -f "$temp_file" - return 1 - fi - else - log_error "Failed to create new agent file" - rm -f "$temp_file" - return 1 - fi - else - # Update existing file - if [[ ! -r "$target_file" ]]; then - log_error "Cannot read existing file: $target_file" - return 1 - fi - - if [[ ! -w "$target_file" ]]; then - log_error "Cannot write to existing file: $target_file" - return 1 - fi - - if update_existing_agent_file "$target_file" "$current_date"; then - log_success "Updated existing $agent_name context file" - else - log_error "Failed to update existing agent file" - return 1 - fi - fi - - return 0 -} - -#============================================================================== -# Agent Selection and Processing -#============================================================================== - -update_specific_agent() { - local agent_type="$1" - - case "$agent_type" in - claude) - update_agent_file "$CLAUDE_FILE" "Claude Code" - ;; - gemini) - update_agent_file "$GEMINI_FILE" "Gemini CLI" - ;; - copilot) - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - ;; - cursor-agent) - update_agent_file "$CURSOR_FILE" "Cursor IDE" - ;; - qwen) - update_agent_file "$QWEN_FILE" "Qwen Code" - ;; - opencode) - update_agent_file "$AGENTS_FILE" "opencode" - ;; - codex) - update_agent_file "$AGENTS_FILE" "Codex CLI" - ;; - windsurf) - update_agent_file "$WINDSURF_FILE" "Windsurf" - ;; - kilocode) - update_agent_file "$KILOCODE_FILE" "Kilo Code" - ;; - auggie) - update_agent_file "$AUGGIE_FILE" "Auggie CLI" - ;; - roo) - update_agent_file "$ROO_FILE" "Roo Code" - ;; - codebuddy) - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" - ;; - qoder) - update_agent_file "$QODER_FILE" "Qoder CLI" - ;; - amp) - update_agent_file "$AMP_FILE" "Amp" - ;; - shai) - update_agent_file "$SHAI_FILE" "SHAI" - ;; - q) - update_agent_file "$Q_FILE" "Amazon Q Developer CLI" - ;; - bob) - update_agent_file "$BOB_FILE" "IBM Bob" - ;; - *) - log_error "Unknown agent type '$agent_type'" - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|shai|q|bob|qoder" - exit 1 - ;; - esac -} - -update_all_existing_agents() { - local found_agent=false - - # Check each possible agent file and update if it exists - if [[ -f "$CLAUDE_FILE" ]]; then - update_agent_file "$CLAUDE_FILE" "Claude Code" - found_agent=true - fi - - if [[ -f "$GEMINI_FILE" ]]; then - update_agent_file "$GEMINI_FILE" "Gemini CLI" - found_agent=true - fi - - if [[ -f "$COPILOT_FILE" ]]; then - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - found_agent=true - fi - - if [[ -f "$CURSOR_FILE" ]]; then - update_agent_file "$CURSOR_FILE" "Cursor IDE" - found_agent=true - fi - - if [[ -f "$QWEN_FILE" ]]; then - update_agent_file "$QWEN_FILE" "Qwen Code" - found_agent=true - fi - - if [[ -f "$AGENTS_FILE" ]]; then - update_agent_file "$AGENTS_FILE" "Codex/opencode" - found_agent=true - fi - - if [[ -f "$WINDSURF_FILE" ]]; then - update_agent_file "$WINDSURF_FILE" "Windsurf" - found_agent=true - fi - - if [[ -f "$KILOCODE_FILE" ]]; then - update_agent_file "$KILOCODE_FILE" "Kilo Code" - found_agent=true - fi - - if [[ -f "$AUGGIE_FILE" ]]; then - update_agent_file "$AUGGIE_FILE" "Auggie CLI" - found_agent=true - fi - - if [[ -f "$ROO_FILE" ]]; then - update_agent_file "$ROO_FILE" "Roo Code" - found_agent=true - fi - - if [[ -f "$CODEBUDDY_FILE" ]]; then - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" - found_agent=true - fi - - if [[ -f "$SHAI_FILE" ]]; then - update_agent_file "$SHAI_FILE" "SHAI" - found_agent=true - fi - - if [[ -f "$QODER_FILE" ]]; then - update_agent_file "$QODER_FILE" "Qoder CLI" - found_agent=true - fi - - if [[ -f "$Q_FILE" ]]; then - update_agent_file "$Q_FILE" "Amazon Q Developer CLI" - found_agent=true - fi - - if [[ -f "$BOB_FILE" ]]; then - update_agent_file "$BOB_FILE" "IBM Bob" - found_agent=true - fi - - # If no agent files exist, create a default Claude file - if [[ "$found_agent" == false ]]; then - log_info "No existing agent files found, creating default Claude file..." - update_agent_file "$CLAUDE_FILE" "Claude Code" - fi -} -print_summary() { - echo - log_info "Summary of changes:" - - if [[ -n "$NEW_LANG" ]]; then - echo " - Added language: $NEW_LANG" - fi - - if [[ -n "$NEW_FRAMEWORK" ]]; then - echo " - Added framework: $NEW_FRAMEWORK" - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then - echo " - Added database: $NEW_DB" - fi - - echo - - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|shai|q|bob|qoder]" -} - -#============================================================================== -# Main Execution -#============================================================================== - -main() { - # Validate environment before proceeding - validate_environment - - log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" - - # Parse the plan file to extract project information - if ! parse_plan_data "$NEW_PLAN"; then - log_error "Failed to parse plan data" - exit 1 - fi - - # Process based on agent type argument - local success=true - - if [[ -z "$AGENT_TYPE" ]]; then - # No specific agent provided - update all existing agent files - log_info "No agent specified, updating all existing agent files..." - if ! update_all_existing_agents; then - success=false - fi - else - # Specific agent provided - update only that agent - log_info "Updating specific agent: $AGENT_TYPE" - if ! update_specific_agent "$AGENT_TYPE"; then - success=false - fi - fi - - # Print summary - print_summary - - if [[ "$success" == true ]]; then - log_success "Agent context update completed successfully" - exit 0 - else - log_error "Agent context update completed with errors" - exit 1 - fi -} - -# Execute main function if script is run directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi - diff --git a/.specify/templates/agent-file-template.md b/.specify/templates/agent-file-template.md deleted file mode 100644 index 4cc7fd66783..00000000000 --- a/.specify/templates/agent-file-template.md +++ /dev/null @@ -1,28 +0,0 @@ -# [PROJECT NAME] Development Guidelines - -Auto-generated from all feature plans. Last updated: [DATE] - -## Active Technologies - -[EXTRACTED FROM ALL PLAN.MD FILES] - -## Project Structure - -```text -[ACTUAL STRUCTURE FROM PLANS] -``` - -## Commands - -[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] - -## Code Style - -[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] - -## Recent Changes - -[LAST 3 FEATURES AND WHAT THEY ADDED] - - - diff --git a/.specify/templates/checklist-template.md b/.specify/templates/checklist-template.md deleted file mode 100644 index 806657da09d..00000000000 --- a/.specify/templates/checklist-template.md +++ /dev/null @@ -1,40 +0,0 @@ -# [CHECKLIST TYPE] Checklist: [FEATURE NAME] - -**Purpose**: [Brief description of what this checklist covers] -**Created**: [DATE] -**Feature**: [Link to spec.md or relevant documentation] - -**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. - - - -## [Category 1] - -- [ ] CHK001 First checklist item with clear action -- [ ] CHK002 Second checklist item -- [ ] CHK003 Third checklist item - -## [Category 2] - -- [ ] CHK004 Another category item -- [ ] CHK005 Item with specific criteria -- [ ] CHK006 Final item in this category - -## Notes - -- Check items off as completed: `[x]` -- Add comments or findings inline -- Link to relevant resources or documentation -- Items are numbered sequentially for easy reference diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md deleted file mode 100644 index 6a8bfc6c8a0..00000000000 --- a/.specify/templates/plan-template.md +++ /dev/null @@ -1,104 +0,0 @@ -# Implementation Plan: [FEATURE] - -**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] -**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` - -**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. - -## Summary - -[Extract from feature spec: primary requirement + technical approach from research] - -## Technical Context - - - -**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] -**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] -**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] -**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] -**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] -**Project Type**: [single/web/mobile - determines source structure] -**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] -**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] -**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] - -## Constitution Check - -*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - -[Gates determined based on constitution file] - -## Project Structure - -### Documentation (this feature) - -```text -specs/[###-feature]/ -├── plan.md # This file (/speckit.plan command output) -├── research.md # Phase 0 output (/speckit.plan command) -├── data-model.md # Phase 1 output (/speckit.plan command) -├── quickstart.md # Phase 1 output (/speckit.plan command) -├── contracts/ # Phase 1 output (/speckit.plan command) -└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) -``` - -### Source Code (repository root) - - -```text -# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) -src/ -├── models/ -├── services/ -├── cli/ -└── lib/ - -tests/ -├── contract/ -├── integration/ -└── unit/ - -# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) -backend/ -├── src/ -│ ├── models/ -│ ├── services/ -│ └── api/ -└── tests/ - -frontend/ -├── src/ -│ ├── components/ -│ ├── pages/ -│ └── services/ -└── tests/ - -# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) -api/ -└── [same as backend above] - -ios/ or android/ -└── [platform-specific structure: feature modules, UI flows, platform tests] -``` - -**Structure Decision**: [Document the selected structure and reference the real -directories captured above] - -## Complexity Tracking - -> **Fill ONLY if Constitution Check has violations that must be justified** - -| Violation | Why Needed | Simpler Alternative Rejected Because | -|-----------|------------|-------------------------------------| -| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | -| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md deleted file mode 100644 index c67d9149807..00000000000 --- a/.specify/templates/spec-template.md +++ /dev/null @@ -1,115 +0,0 @@ -# Feature Specification: [FEATURE NAME] - -**Feature Branch**: `[###-feature-name]` -**Created**: [DATE] -**Status**: Draft -**Input**: User description: "$ARGUMENTS" - -## User Scenarios & Testing *(mandatory)* - - - -### User Story 1 - [Brief Title] (Priority: P1) - -[Describe this user journey in plain language] - -**Why this priority**: [Explain the value and why it has this priority level] - -**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"] - -**Acceptance Scenarios**: - -1. **Given** [initial state], **When** [action], **Then** [expected outcome] -2. **Given** [initial state], **When** [action], **Then** [expected outcome] - ---- - -### User Story 2 - [Brief Title] (Priority: P2) - -[Describe this user journey in plain language] - -**Why this priority**: [Explain the value and why it has this priority level] - -**Independent Test**: [Describe how this can be tested independently] - -**Acceptance Scenarios**: - -1. **Given** [initial state], **When** [action], **Then** [expected outcome] - ---- - -### User Story 3 - [Brief Title] (Priority: P3) - -[Describe this user journey in plain language] - -**Why this priority**: [Explain the value and why it has this priority level] - -**Independent Test**: [Describe how this can be tested independently] - -**Acceptance Scenarios**: - -1. **Given** [initial state], **When** [action], **Then** [expected outcome] - ---- - -[Add more user stories as needed, each with an assigned priority] - -### Edge Cases - - - -- What happens when [boundary condition]? -- How does system handle [error scenario]? - -## Requirements *(mandatory)* - - - -### Functional Requirements - -- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] -- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] -- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] -- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] -- **FR-005**: System MUST [behavior, e.g., "log all security events"] - -*Example of marking unclear requirements:* - -- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] -- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] - -### Key Entities *(include if feature involves data)* - -- **[Entity 1]**: [What it represents, key attributes without implementation] -- **[Entity 2]**: [What it represents, relationships to other entities] - -## Success Criteria *(mandatory)* - - - -### Measurable Outcomes - -- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"] -- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"] -- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"] -- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"] diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md deleted file mode 100644 index 60f9be455d2..00000000000 --- a/.specify/templates/tasks-template.md +++ /dev/null @@ -1,251 +0,0 @@ ---- - -description: "Task list template for feature implementation" ---- - -# Tasks: [FEATURE NAME] - -**Input**: Design documents from `/specs/[###-feature-name]/` -**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ - -**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification. - -**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. - -## Format: `[ID] [P?] [Story] Description` - -- **[P]**: Can run in parallel (different files, no dependencies) -- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) -- Include exact file paths in descriptions - -## Path Conventions - -- **Single project**: `src/`, `tests/` at repository root -- **Web app**: `backend/src/`, `frontend/src/` -- **Mobile**: `api/src/`, `ios/src/` or `android/src/` -- Paths shown below assume single project - adjust based on plan.md structure - - - -## Phase 1: Setup (Shared Infrastructure) - -**Purpose**: Project initialization and basic structure - -- [ ] T001 Create project structure per implementation plan -- [ ] T002 Initialize [language] project with [framework] dependencies -- [ ] T003 [P] Configure linting and formatting tools - ---- - -## Phase 2: Foundational (Blocking Prerequisites) - -**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented - -**⚠️ CRITICAL**: No user story work can begin until this phase is complete - -Examples of foundational tasks (adjust based on your project): - -- [ ] T004 Setup database schema and migrations framework -- [ ] T005 [P] Implement authentication/authorization framework -- [ ] T006 [P] Setup API routing and middleware structure -- [ ] T007 Create base models/entities that all stories depend on -- [ ] T008 Configure error handling and logging infrastructure -- [ ] T009 Setup environment configuration management - -**Checkpoint**: Foundation ready - user story implementation can now begin in parallel - ---- - -## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP - -**Goal**: [Brief description of what this story delivers] - -**Independent Test**: [How to verify this story works on its own] - -### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️ - -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** - -- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py -- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py - -### Implementation for User Story 1 - -- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py -- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py -- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013) -- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py -- [ ] T016 [US1] Add validation and error handling -- [ ] T017 [US1] Add logging for user story 1 operations - -**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently - ---- - -## Phase 4: User Story 2 - [Title] (Priority: P2) - -**Goal**: [Brief description of what this story delivers] - -**Independent Test**: [How to verify this story works on its own] - -### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ - -- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py -- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py - -### Implementation for User Story 2 - -- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py -- [ ] T021 [US2] Implement [Service] in src/services/[service].py -- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py -- [ ] T023 [US2] Integrate with User Story 1 components (if needed) - -**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently - ---- - -## Phase 5: User Story 3 - [Title] (Priority: P3) - -**Goal**: [Brief description of what this story delivers] - -**Independent Test**: [How to verify this story works on its own] - -### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ - -- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py -- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py - -### Implementation for User Story 3 - -- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py -- [ ] T027 [US3] Implement [Service] in src/services/[service].py -- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py - -**Checkpoint**: All user stories should now be independently functional - ---- - -[Add more user story phases as needed, following the same pattern] - ---- - -## Phase N: Polish & Cross-Cutting Concerns - -**Purpose**: Improvements that affect multiple user stories - -- [ ] TXXX [P] Documentation updates in docs/ -- [ ] TXXX Code cleanup and refactoring -- [ ] TXXX Performance optimization across all stories -- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/ -- [ ] TXXX Security hardening -- [ ] TXXX Run quickstart.md validation - ---- - -## Dependencies & Execution Order - -### Phase Dependencies - -- **Setup (Phase 1)**: No dependencies - can start immediately -- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories -- **User Stories (Phase 3+)**: All depend on Foundational phase completion - - User stories can then proceed in parallel (if staffed) - - Or sequentially in priority order (P1 → P2 → P3) -- **Polish (Final Phase)**: Depends on all desired user stories being complete - -### User Story Dependencies - -- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories -- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable -- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable - -### Within Each User Story - -- Tests (if included) MUST be written and FAIL before implementation -- Models before services -- Services before endpoints -- Core implementation before integration -- Story complete before moving to next priority - -### Parallel Opportunities - -- All Setup tasks marked [P] can run in parallel -- All Foundational tasks marked [P] can run in parallel (within Phase 2) -- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows) -- All tests for a user story marked [P] can run in parallel -- Models within a story marked [P] can run in parallel -- Different user stories can be worked on in parallel by different team members - ---- - -## Parallel Example: User Story 1 - -```bash -# Launch all tests for User Story 1 together (if tests requested): -Task: "Contract test for [endpoint] in tests/contract/test_[name].py" -Task: "Integration test for [user journey] in tests/integration/test_[name].py" - -# Launch all models for User Story 1 together: -Task: "Create [Entity1] model in src/models/[entity1].py" -Task: "Create [Entity2] model in src/models/[entity2].py" -``` - ---- - -## Implementation Strategy - -### MVP First (User Story 1 Only) - -1. Complete Phase 1: Setup -2. Complete Phase 2: Foundational (CRITICAL - blocks all stories) -3. Complete Phase 3: User Story 1 -4. **STOP and VALIDATE**: Test User Story 1 independently -5. Deploy/demo if ready - -### Incremental Delivery - -1. Complete Setup + Foundational → Foundation ready -2. Add User Story 1 → Test independently → Deploy/Demo (MVP!) -3. Add User Story 2 → Test independently → Deploy/Demo -4. Add User Story 3 → Test independently → Deploy/Demo -5. Each story adds value without breaking previous stories - -### Parallel Team Strategy - -With multiple developers: - -1. Team completes Setup + Foundational together -2. Once Foundational is done: - - Developer A: User Story 1 - - Developer B: User Story 2 - - Developer C: User Story 3 -3. Stories complete and integrate independently - ---- - -## Notes - -- [P] tasks = different files, no dependencies -- [Story] label maps task to specific user story for traceability -- Each user story should be independently completable and testable -- Verify tests fail before implementing -- Commit after each task or logical group -- Stop at any checkpoint to validate story independently -- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence diff --git a/README.adoc b/README.adoc index 7f22a5bb2be..29367828e32 100644 --- a/README.adoc +++ b/README.adoc @@ -59,16 +59,16 @@ $ ./mvnw clean install -Dquickly == Contribute This is an open source project, and you are more than welcome to contribute! -For more, see link:CONTRIBUTING.adoc[Contributing]. +For more, see link:CONTRIBUTING.md[Contributing]. == Editions There are two editions of Timefold Solver: -- Timefold Solver Community Edition (this repo). -- Timefold Solver Enterprise Edition, a https://timefold.ai/pricing[licensed version] of Timefold Solver. +- _Timefold Solver Community Edition_ (this repo). +- _Timefold Solver Enterprise Edition_, a https://timefold.ai/pricing[licensed version] of Timefold Solver. -=== Key Features of Timefold Solver Enterprise Edition (EE) +=== Key Features of Timefold Solver Enterprise Edition - **Multi-threaded Solving:** Experience enhanced performance with multi-threaded solving capabilities. - **Nearby Selection:** Get better solutions quicker, especially with spatial problems. @@ -76,8 +76,8 @@ There are two editions of Timefold Solver: === Licensing and Usage -Unlike the Apache-2.0 licensed Community Edition in this repo, -the Enterprise Edition is not open source. +Unlike the Apache-2.0 licensed _Community Edition_ in this repo, +the _Enterprise Edition_ is not open source. If you wish to use the Enterprise Edition in a production environment, please https://timefold.ai/contact[contact Timefold] to obtain the appropriate license. diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java b/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java deleted file mode 100644 index 8b654dab9db..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java +++ /dev/null @@ -1,134 +0,0 @@ -package ai.timefold.solver.core.api.score; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.api.score.stream.Constraint; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; -import ai.timefold.solver.core.api.solver.SolutionManager; - -import org.jspecify.annotations.NullMarked; - -/** - * Build by {@link SolutionManager#explain(Object)} to hold {@link ConstraintMatchTotal}s and {@link Indictment}s - * necessary to explain the quality of a particular {@link Score}. - *

- * For a simplified, faster and JSON-friendly alternative, see {@link ScoreAnalysis}. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @param the actual score type - */ -@NullMarked -public interface ScoreExplanation> { - - /** - * Retrieve the {@link PlanningSolution} that the score being explained comes from. - */ - Solution_ getSolution(); - - /** - * Return the {@link Score} being explained. - * If the specific {@link Score} type used by the {@link PlanningSolution} is required, - * call {@link #getSolution()} and retrieve it from there. - */ - Score_ getScore(); - - /** - * Whether {@link #getSolution()} is initialized or not. - * - * @return true if initialized - */ - boolean isInitialized(); - - /** - * Returns a diagnostic text that explains the solution through the {@link ConstraintMatch} API to identify which - * constraints or planning entities cause that score quality. - *

- * In case of an {@link Score#isFeasible() infeasible} solution, this can help diagnose the cause of that. - * - *

- * Do not parse the return value, its format may change without warning. - * Instead, to provide this information in a UI or a service, - * use {@link ScoreExplanation#getConstraintMatchTotalMap()} and {@link ScoreExplanation#getIndictmentMap()} - * and convert those into a domain-specific API. - */ - String getSummary(); - - /** - * Explains the {@link Score} of {@link #getScore()} by splitting it up per {@link Constraint}. - *

- * The sum of {@link ConstraintMatchTotal#getScore()} equals {@link #getScore()}. - * - * @return the key is the constraint name - * @see #getIndictmentMap() - */ - Map> getConstraintMatchTotalMap(); - - /** - * Explains the {@link Score} of {@link #getScore()} for all constraints. - * The return value of this method is determined by several factors: - * - *

    - *
  • - * With Constraint Streams, the user has an option to provide a custom justification mapping, - * implementing {@link ConstraintJustification}. - * If provided, every {@link ConstraintMatch} of such constraint will be associated with this custom justification class. - * Every constraint not associated with a custom justification class - * will be associated with {@link DefaultConstraintJustification}. - *
  • - *
  • - * With {@link ConstraintMatchAwareIncrementalScoreCalculator}, - * every {@link ConstraintMatch} will be associated with the justification class that the user created it with. - *
  • - *
- * - * @return all constraint matches - * @see #getIndictmentMap() - */ - List getJustificationList(); - - /** - * Explains the {@link Score} of {@link #getScore()} for all constraints - * justified with a given {@link ConstraintJustification} type. - * Otherwise, as defined by {@link #getJustificationList()}. - * May be empty, if the score explanation ran with justification support disabled. - * - * @return all constraint matches associated with the given justification class - * @see #getIndictmentMap() - */ - default List - getJustificationList(Class constraintJustificationClass) { - return getJustificationList() - .stream() - .filter(constraintJustification -> constraintJustificationClass - .isAssignableFrom(constraintJustification.getClass())) - .map(constraintJustification -> (ConstraintJustification_) constraintJustification) - .collect(Collectors.toList()); - } - - /** - * Explains the impact of each planning entity or problem fact on the {@link Score}. - * An {@link Indictment} is basically the inverse of a {@link ConstraintMatchTotal}: - * it is a {@link Score} total for any of the {@link ConstraintMatch#getIndictedObjectList() indicted objects}. - *

- * The sum of {@link ConstraintMatchTotal#getScore()} differs from {@link #getScore()} - * because each {@link ConstraintMatch#getScore()} is counted - * for each of the {@link ConstraintMatch#getIndictedObjectList() indicted objects}. - * - * @return the key is a {@link ProblemFactCollectionProperty problem fact} or a - * {@link PlanningEntity planning entity} - * @see #getConstraintMatchTotalMap() - */ - Map> getIndictmentMap(); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java index 671e1378193..2b56f5de00d 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java @@ -1,266 +1,78 @@ package ai.timefold.solver.core.api.score.analysis; -import static ai.timefold.solver.core.api.score.analysis.ScoreAnalysis.DEFAULT_SUMMARY_CONSTRAINT_MATCH_LIMIT; -import static java.util.Comparator.comparing; - -import java.util.Comparator; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.solver.SolutionManager; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** - * Note: Users should never create instances of this type directly. - * It is available transitively via {@link SolutionManager#analyze(Object)}. - * - * @param - * @param matches null if analysis not available; - * empty if constraint has no matches, but still non-zero constraint weight; - * non-empty if constraint has matches. - * This is a {@link List} to simplify access to individual elements, - * but it contains no duplicates just like {@link HashSet} wouldn't. - * @param matchCount - *

    - *
  • For regular constraint analysis: - * -1 if analysis not available, - * 0 if constraint has no matches, - * positive if constraint has matches. - * Equal to the size of the {@link #matches} list.
  • - *
  • For a {@link ScoreAnalysis#diff(ScoreAnalysis) diff of constraint analyses}: - * positive if the constraint has more matches in the new analysis, - * zero if the number of matches is the same in both, - * negative otherwise. - * Need not be equal to the size of the {@link #matches} list.
  • - *
+ * Note: {@link ScoreAnalysis} is exclusive to Timefold Solver Enterprise Edition. + * + * @see ScoreAnalysis Description of score analysis and the purpose of this class. */ @NullMarked -public record ConstraintAnalysis>(ConstraintRef constraintRef, Score_ weight, Score_ score, - @Nullable List> matches, int matchCount) { - - public ConstraintAnalysis(ConstraintRef constraintRef, Score_ weight, Score_ score, - @Nullable List> matches) { - this(constraintRef, weight, score, matches, matches == null ? -1 : matches.size()); - } - - public ConstraintAnalysis { - Objects.requireNonNull(constraintRef); - /* - * Null only possible in ConstraintMatchAwareIncrementalScoreCalculator and/or tests. - * Easy doesn't support constraint analysis at all. - * CS always provides constraint weights. - */ - Objects.requireNonNull(weight); - Objects.requireNonNull(score); - } - - ConstraintAnalysis negate() { - // Only used to compute diff; use semantics for non-diff. - // A negative match count is only allowed within these semantics when matches == null. - if (matches == null) { - // At this point, matchCount is already negative, as matches == null. - return new ConstraintAnalysis<>(constraintRef, weight.negate(), score.negate(), null, matchCount); - } else { - // Within these semantics, match count == list size. - var negatedMatchAnalysesList = matches.stream() - .map(MatchAnalysis::negate) - .toList(); - return new ConstraintAnalysis<>(constraintRef, weight.negate(), score.negate(), negatedMatchAnalysesList, - matchCount); - } - } +public interface ConstraintAnalysis> { - static > ConstraintAnalysis diff( - ConstraintRef constraintRef, @Nullable ConstraintAnalysis constraintAnalysis, - @Nullable ConstraintAnalysis otherConstraintAnalysis) { - if (constraintAnalysis == null) { - if (otherConstraintAnalysis == null) { - throw new IllegalStateException( - "Impossible state: none of the score explanations provided constraint matches for a constraint (%s)." - .formatted(constraintRef)); - } - // No need to compute diff; this constraint is not present in this score explanation. - return otherConstraintAnalysis.negate(); - } else if (otherConstraintAnalysis == null) { - // No need to compute diff; this constraint is not present in the other score explanation. - return constraintAnalysis; - } - var matchAnalyses = constraintAnalysis.matches(); - var otherMatchAnalyses = otherConstraintAnalysis.matches(); - if ((matchAnalyses == null && otherMatchAnalyses != null) || (matchAnalyses != null && otherMatchAnalyses == null)) { - throw new IllegalStateException( - "Impossible state: One of the score analyses (%s, %s) provided no match analysis for a constraint (%s)." - .formatted(constraintAnalysis, otherConstraintAnalysis, constraintRef)); - } - // Compute the diff. - var constraintWeightDifference = constraintAnalysis.weight().subtract(otherConstraintAnalysis.weight()); - var scoreDifference = constraintAnalysis.score().subtract(otherConstraintAnalysis.score()); - if (matchAnalyses == null) { - var leftHasMatchCount = hasMatchCount(constraintAnalysis); - var rightHasMatchCount = hasMatchCount(otherConstraintAnalysis); - if ((!leftHasMatchCount && rightHasMatchCount) || (leftHasMatchCount && !rightHasMatchCount)) { - throw new IllegalStateException( - "Impossible state: One of the score analyses (%s, %s) provided no match count for a constraint (%s)." - .formatted(constraintAnalysis, otherConstraintAnalysis, constraintRef)); - } - return new ConstraintAnalysis<>(constraintRef, constraintWeightDifference, scoreDifference, null, - getMatchCount(constraintAnalysis, otherConstraintAnalysis)); - } - var matchAnalysisMap = mapMatchesToJustifications(matchAnalyses); - var otherMatchAnalysisMap = mapMatchesToJustifications(otherMatchAnalyses); - var matchAnalysesList = Stream.concat(matchAnalysisMap.keySet().stream(), otherMatchAnalysisMap.keySet().stream()) - .distinct() - .flatMap(justification -> { - var matchAnalysis = matchAnalysisMap.get(justification); - var otherMatchAnalysis = otherMatchAnalysisMap.get(justification); - if (matchAnalysis == null) { - if (otherMatchAnalysis == null) { - throw new IllegalStateException( - "Impossible state: none of the match analyses provided for a constraint (%s)." - .formatted(constraintRef)); - } - // No need to compute diff; this match is not present in this score explanation. - return Stream.of(otherMatchAnalysis.negate()); - } else if (otherMatchAnalysis == null) { - // No need to compute diff; this match is not present in the other score explanation. - return Stream.of(matchAnalysis); - } else if (!matchAnalysis.equals(otherMatchAnalysis)) { // Compute the diff. - return Stream.of(new MatchAnalysis<>(constraintRef, - matchAnalysis.score().subtract(otherMatchAnalysis.score()), justification)); - } else { // There is no difference; skip entirely. - return Stream.empty(); - } - }).toList(); - return new ConstraintAnalysis<>(constraintRef, constraintWeightDifference, scoreDifference, matchAnalysesList, - getMatchCount(constraintAnalysis, otherConstraintAnalysis)); - } + ConstraintRef constraintRef(); - private static boolean hasMatchCount(ConstraintAnalysis analysis) { - return analysis.matchCount >= 0; - } + Score_ weight(); - private static int getMatchCount(ConstraintAnalysis analysis, ConstraintAnalysis otherAnalysis) { - return analysis.matchCount() - otherAnalysis.matchCount(); - } + Score_ score(); - private static > Map> - mapMatchesToJustifications(List> matchAnalyses) { + /** + * + * @return null if analysis not available; + * empty if constraint has no matches, but still non-zero constraint weight; + * non-empty if constraint has matches. + * This is a {@link List} to simplify access to individual elements, + * but it contains no duplicates just like {@link HashSet} wouldn't. + */ + @Nullable + List> matches(); - Map> matchAnalysisMap = - LinkedHashMap.newLinkedHashMap(matchAnalyses.size()); - for (var matchAnalysis : matchAnalyses) { - var previous = matchAnalysisMap.put(matchAnalysis.justification(), matchAnalysis); - if (previous != null) { - // Match analysis for the same justification should have been merged already. - throw new IllegalStateException( - "Impossible state: multiple constraint matches (%s, %s) have the same justification (%s)." - .formatted(previous, matchAnalysis, matchAnalysis.justification())); - } - } - return matchAnalysisMap; - } + /** + * @return + *
    + *
  • For regular constraint analysis: + * -1 if analysis not available, + * 0 if constraint has no matches, + * positive if constraint has matches. + * Equal to the size of the {@link #matches} list.
  • + *
  • For a {@link ScoreAnalysis#diff(ScoreAnalysis) diff of constraint analyses}: + * positive if the constraint has more matches in the new analysis, + * zero if the number of matches is the same in both, + * negative otherwise. + * Need not be equal to the size of the {@link #matches} list.
  • + *
+ */ + int matchCount(); /** * Return name of the constraint that this analysis is for. * * @return equal to {@code constraintRef.constraintName()} */ - public String constraintName() { - return constraintRef.constraintName(); - } + String constraintName(); /** * Returns a diagnostic text that explains part of the score quality through the {@link ConstraintAnalysis} API. - * The string is built fresh every time the method is called. + * The summary will be limited to the first 3 matches per constraint; + * see {@link #summarize(int)} to specify a custom limit. + * The summary is built fresh every time the method is called. */ - @SuppressWarnings("java:S3457") - public String summarize() { - return buildSummary(DEFAULT_SUMMARY_CONSTRAINT_MATCH_LIMIT); - } + String summarize(); /** - * Return a diagnostic text that explains part of the score quality in a similar way to {@link #summarize()}. - * It is possible to specify the maximum number of constraint matches to display per constraint by passing - * the desired limit as an argument. + * Return a diagnostic text that explains part of the score quality as defined by {@link #summarize()}, + * but allows specifying the maximum number of constraint matches to display per constraint. * * @param topLimit maximum number of constraint matches to display per constraint * Use {@link Integer#MAX_VALUE} to show all matches. */ + String summarize(int topLimit); - public String summarize(int topLimit) { - if (topLimit < 1) { - throw new IllegalArgumentException("The topLimit (%d) must be at least 1.".formatted(topLimit)); - } - return buildSummary(topLimit); - } - - @SuppressWarnings("java:S3457") - private String buildSummary(int topLimit) { - var summary = new StringBuilder(); - summary.append(""" - Explanation of score (%s): - Constraint matches: - """.formatted(score)); - Comparator> matchScoreComparator = comparing(MatchAnalysis::score); - - var constraintMatches = matches(); - if (constraintMatches == null) { - throw new IllegalArgumentException(""" - The constraint matches must be non-null. - Maybe use ScoreAnalysisFetchPolicy.FETCH_ALL to request the score analysis - """); - } - if (constraintMatches.isEmpty()) { - summary.append( - "%8s%s: constraint (%s) has no matches.\n".formatted(" ", - score().toShortString(), - constraintRef().constraintName())); - } else { - summary.append( - "%8s%s: constraint (%s) has %s matches:\n".formatted(" ", - score().toShortString(), - constraintRef().constraintName(), - constraintMatches.size())); - } - - constraintMatches.stream() - .sorted(matchScoreComparator) - .limit(topLimit) - .forEach(match -> summary.append( - "%12s%s: justified with (%s)\n".formatted(" ", - match.score().toShortString(), - match.justification()))); - - if (constraintMatches.size() > topLimit) { - summary.append("%12s... and %d more matches\n".formatted(" ", - constraintMatches.size() - topLimit)); - } - - return summary.toString(); - } - - @Override - public String toString() { - if (matches == null) { - if (matchCount == -1) { - return "(%s at %s, constraint matching disabled)" - .formatted(score, weight); - } else { - return "(%s at %s, %d matches, justifications disabled)" - .formatted(score, weight, matchCount); - } - } else { - return "(%s at %s, %d matches with justifications)" - .formatted(score, weight, matches.size()); - } - } } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java index 22fbfb76ade..ee19a8b4a59 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java @@ -1,56 +1,25 @@ package ai.timefold.solver.core.api.score.analysis; -import java.util.Objects; - import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.ConstraintProvider; -import ai.timefold.solver.core.api.solver.SolutionManager; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import org.jspecify.annotations.NullMarked; /** - * Note: Users should never create instances of this type directly. - * It is available transitively via {@link SolutionManager#analyze(Object)}. + * Note: {@link ScoreAnalysis} is exclusive to Timefold Solver Enterprise Edition. * * @param + * @see ScoreAnalysis Description of score analysis and the purpose of this class. */ @NullMarked -public record MatchAnalysis>(ConstraintRef constraintRef, Score_ score, - ConstraintJustification justification) implements Comparable> { +public interface MatchAnalysis> + extends Comparable> { + + ConstraintRef constraintRef(); - public MatchAnalysis { - Objects.requireNonNull(constraintRef); - Objects.requireNonNull(score); - // Null justification is impossible; - // if the fetch policy doesn't require match analysis, the code shouldn't even get here. - Objects.requireNonNull(justification, () -> """ - Impossible state: Received a null justification. - Maybe check your %s's justifyWith() implementation for that constraint?""" - .formatted(ConstraintProvider.class)); - } + Score_ score(); - MatchAnalysis negate() { - return new MatchAnalysis<>(constraintRef, score.negate(), justification); - } + ConstraintJustification justification(); - @Override - public int compareTo(MatchAnalysis other) { - int constraintRefComparison = this.constraintRef.compareTo(other.constraintRef); - if (constraintRefComparison != 0) { - return constraintRefComparison; - } - int scoreComparison = this.score.compareTo(other.score); - if (scoreComparison != 0) { - return scoreComparison; - } else { - if (this.justification instanceof Comparable comparableJustification - && other.justification instanceof Comparable otherComparableJustification) { - return comparableJustification.compareTo(otherComparableJustification); - } else { - return 0; - } - } - } } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index 5c7a7c1d285..22ee39b2ea4 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -1,23 +1,12 @@ package ai.timefold.solver.core.api.score.analysis; -import static java.util.Comparator.comparing; - import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.SequencedMap; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.api.solver.SolutionManager; @@ -27,7 +16,6 @@ /** * Represents the breakdown of a {@link Score} into individual {@link ConstraintAnalysis} instances, * one for each constraint. - * Compared to {@link ScoreExplanation}, this is JSON-friendly and faster to generate. * *

* In order to be fully serializable to JSON, {@link MatchAnalysis} instances must be serializable to JSON @@ -42,78 +30,61 @@ * and so they are the correct party to provide the deserializer. * *

- * Note: the constructors of this record are off-limits. - * We ask users to use exclusively {@link SolutionManager#analyze(Object)} to obtain instances of this record. - * - * @param score Score of the solution being analyzed. - * @param constraintMap for each constraint identified by its {@link Constraint#getConstraintRef()}, - * the {@link ConstraintAnalysis} that describes the impact of that constraint on the overall score. - *

- * Zero-weight constraints are never included, they are excluded from score calculation in the first place. - * Otherwise constraints are always included, even if they have no matches, - * unless the score analysis represents a diff between two other analyses. - * - *

- * In the case of a diff: - * - *

    - *
  • If the constraint weight diff is non-zero, - * or if the score diff for the constraint is non-zero, - * the constraint diff will be included.
  • - *
  • - * Otherwise if constraint matching is disabled ({@link ScoreAnalysisFetchPolicy#FETCH_SHALLOW}) - * or if only match counts are available ({@link ScoreAnalysisFetchPolicy#FETCH_MATCH_COUNT}), - * constraint diff will only be included if it has a non-zero match count diff. - *
  • - *
  • - * Otherwise (when constraint matching is fully enabled with {@link ScoreAnalysisFetchPolicy#FETCH_ALL}) - * the constraint diff will not be included if the diff of its constraint matches is empty. - * (In other words: when diffing, the analysis for a particular constraint won't be available - * if we can guarantee that the constraint matches are identical in both analyses.) - *
  • - *
- * - *

- * Entries in the map have a stable iteration order; items are ordered first by {@link ConstraintAnalysis#weight()}, - * then by {@link ConstraintAnalysis#constraintRef()}. - * @param isSolutionInitialized Whether the solution was fully initialized at the time of analysis. + * Note: {@link ScoreAnalysis} is exclusive to Timefold Solver Enterprise Edition. * * @param + * @see SolutionManager#analyze(Object) Score analysis is typically obtained through SolutionManager. */ @NullMarked -public record ScoreAnalysis>(Score_ score, - Map> constraintMap, boolean isSolutionInitialized) { - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static final Comparator> REVERSED_WEIGHT_COMPARATOR = - Comparator., Score> comparing(ConstraintAnalysis::weight) - .reversed(); - private static final Comparator> MAP_COMPARATOR = - REVERSED_WEIGHT_COMPARATOR.thenComparing(ConstraintAnalysis::constraintRef); +public interface ScoreAnalysis> { - static final int DEFAULT_SUMMARY_CONSTRAINT_MATCH_LIMIT = 3; + /** + * The overall score of the solution. + * This is the score that the individual {@link ConstraintAnalysis} instances sum up to. + * + * @return score of the solution being analyzed + */ + Score_ score(); /** - * As defined by {@link #ScoreAnalysis(Score, Map, boolean)}, - * with the final argument set to true. + * For each constraint identified by its {@link Constraint#getConstraintRef()}, + * the {@link ConstraintAnalysis} that describes the impact of that constraint on the overall score. + *

+ * Zero-weight constraints are never included, they are excluded from score calculation in the first place. + * Otherwise constraints are always included, even if they have no matches, + * unless the score analysis represents a diff between two other analyses. + * + *

+ * In the case of a diff: + * + *

    + *
  • If the constraint weight diff is non-zero, + * or if the score diff for the constraint is non-zero, + * the constraint diff will be included.
  • + *
  • + * Otherwise if constraint matching is disabled ({@link ScoreAnalysisFetchPolicy#FETCH_SHALLOW}) + * or if only match counts are available ({@link ScoreAnalysisFetchPolicy#FETCH_MATCH_COUNT}), + * constraint diff will only be included if it has a non-zero match count diff. + *
  • + *
  • + * Otherwise (when constraint matching is fully enabled with {@link ScoreAnalysisFetchPolicy#FETCH_ALL}) + * the constraint diff will not be included if the diff of its constraint matches is empty. + * (In other words: when diffing, the analysis for a particular constraint won't be available + * if we can guarantee that the constraint matches are identical in both analyses.) + *
  • + *
+ * + * @return Entries have a stable iteration order; items are ordered first by {@link ConstraintAnalysis#weight()}, + * then by {@link ConstraintAnalysis#constraintRef()}. */ - public ScoreAnalysis(Score_ score, Map> constraintMap) { - this(score, constraintMap, true); - } + SequencedMap> constraintMap(); - public ScoreAnalysis { - Objects.requireNonNull(score, "score"); - Objects.requireNonNull(constraintMap, "constraintMap"); - // Ensure consistent order and no external interference. - constraintMap = Collections.unmodifiableMap(constraintMap.values() - .stream() - .sorted(MAP_COMPARATOR) - .collect(Collectors.toMap( - ConstraintAnalysis::constraintRef, - Function.identity(), - (constraintAnalysis, otherConstraintAnalysis) -> constraintAnalysis, - LinkedHashMap::new))); - } + /** + * Indicates whether the solution was fully initialized at the time of analysis. + * + * @return isSolutionInitialized true if the solution was fully initialized at the time of analysis. + */ + boolean isSolutionInitialized(); /** * Performs a lookup on {@link #constraintMap()}. @@ -121,28 +92,16 @@ public ScoreAnalysis(Score_ score, Map * * @return null if no constraint matches of such constraint are present */ - public @Nullable ConstraintAnalysis getConstraintAnalysis(ConstraintRef constraintRef) { - return constraintMap.get(constraintRef); - } + @Nullable + ConstraintAnalysis getConstraintAnalysis(ConstraintRef constraintRef); /** * As defined by {@link #getConstraintAnalysis(ConstraintRef)}. * * @return null if no constraint matches of such constraint are present */ - public @Nullable ConstraintAnalysis getConstraintAnalysis(String constraintName) { - var constraintAnalysisList = constraintMap.entrySet() - .stream() - .filter(entry -> entry.getKey().constraintName().equals(constraintName)) - .map(Map.Entry::getValue) - .toList(); - return switch (constraintAnalysisList.size()) { - case 0 -> null; - case 1 -> constraintAnalysisList.getFirst(); - default -> throw new IllegalStateException( - "Impossible state: Multiple constraints with the same name (%s) are present in the score analysis."); - }; - } + @Nullable + ConstraintAnalysis getConstraintAnalysis(String constraintName); /** * Compare this {@link ScoreAnalysis} to another {@link ScoreAnalysis} @@ -163,70 +122,27 @@ public ScoreAnalysis(Score_ score, Map *

* If one {@link ScoreAnalysis} provides {@link MatchAnalysis} and the other doesn't, exception is thrown. * Such {@link ScoreAnalysis} instances are mutually incompatible. - * + * *

* If {@code this} came from a fully initialized solution, * {@link #isSolutionInitialized} will be true. * False otherwise. */ - public ScoreAnalysis diff(ScoreAnalysis other) { - var result = Stream.concat(constraintMap.keySet().stream(), - other.constraintMap.keySet().stream()) - .distinct() - .flatMap(constraintRef -> { - var constraintAnalysis = getConstraintAnalysis(constraintRef); - var otherConstraintAnalysis = other.getConstraintAnalysis(constraintRef); - var diff = ConstraintAnalysis.diff(constraintRef, constraintAnalysis, otherConstraintAnalysis); - // The following code implements logic to decide which information the user needs to see, - // and which is information we can safely discard. - // This is done so that the diff (which is likely to be serialized into JSON) is not bloated. - if (!diff.weight().isZero() || !diff.score().isZero()) { // Guaranteed change. - return Stream.of(diff); - } - // Figuring out whether constraint matches changed is tricky. - // Can't use constraint weight; weight diff on the same constraint is zero if weight unchanged. - // Can't use matchCount; matchCount diff can be zero if one match was added and another removed. - // To detect if the constraint matches changed, we use the actual match diff. - if (diff.matches() == null) { - // If it is null, either justifications are disabled, - // or constraint matching is disabled altogether. - // This means we don't have enough information to make smarter decisions. - if (diff.matchCount() == 0) { - // Returning this makes no practical sense. - // The result would be constraint name + zero weight + zero score + zero match count. - return Stream.empty(); - } else { - return Stream.of(diff); - } - } else if (!diff.matches().isEmpty()) { - // We actually have constraint matches, and they are meaningfully different. - return Stream.of(diff); - } else { - // This will be empty only if all matches are exactly the same. - return Stream.empty(); - } - }) - .collect(Collectors.toMap( - ConstraintAnalysis::constraintRef, - Function.identity(), - (constraintRef, otherConstraintRef) -> constraintRef, - HashMap::new)); - return new ScoreAnalysis<>(score.subtract(other.score()), result, isSolutionInitialized); - } + ScoreAnalysis diff(ScoreAnalysis other); /** * Returns individual {@link ConstraintAnalysis} instances that make up this {@link ScoreAnalysis}. * * @return equivalent to {@code constraintMap().values()} */ - public Collection> constraintAnalyses() { - return constraintMap.values(); - } + Collection> constraintAnalyses(); /** * Returns a diagnostic text that explains the solution through the {@link ConstraintAnalysis} API to identify which * constraints cause that score quality. - * The string is built fresh every time the method is called. + * The summary will be limited to the first 3 matches per constraint; + * see {@link #summarize(int)} to specify a custom limit. + * The summary is built fresh every time the method is called. *

* In case of an {@link Score#isFeasible() infeasible} solution, this can help diagnose the cause of that. * @@ -236,75 +152,17 @@ public Collection> constraintAnalyses() { * use {@link ScoreAnalysis#constraintAnalyses()} * and convert those into a domain-specific API. */ - @SuppressWarnings("java:S3457") - public String summarize() { - return buildSummary(DEFAULT_SUMMARY_CONSTRAINT_MATCH_LIMIT); - } + String summarize(); /** - * Provides a summary of the solution's score analysis in a similar way to {@link #summarize()}. + * Provides a summary of the solution's score analysis as defined by {@link #summarize()}, + * but allows specifying the maximum number of constraint matches to display per constraint. * It is possible to specify the maximum number of constraint matches to display per constraint by passing * the desired limit as an argument. * * @param topLimit maximum number of constraint matches to show per constraint. * Use {@link Integer#MAX_VALUE} to show all matches. */ - @SuppressWarnings("java:S3457") - public String summarize(int topLimit) { - if (topLimit < 1) { - throw new IllegalArgumentException("The topLimit (%d) must be at least 1.".formatted(topLimit)); - } - return buildSummary(topLimit); - } - - @SuppressWarnings("java:S3457") - private String buildSummary(int topLimit) { - StringBuilder summary = new StringBuilder(); - summary.append(""" - Explanation of score (%s): - Constraint matches: - """.formatted(score)); - Comparator> constraintsScoreComparator = comparing(ConstraintAnalysis::score); - Comparator> matchScoreComparator = comparing(MatchAnalysis::score); - - constraintAnalyses().stream() - .sorted(constraintsScoreComparator) - .forEach(constraint -> { - var matches = constraint.matches(); - if (matches == null) { - throw new IllegalArgumentException(""" - The constraint matches must be non-null. - Maybe use ScoreAnalysisFetchPolicy.FETCH_ALL to request the score analysis - """); - } - if (matches.isEmpty()) { - summary.append( - "%8s%s: constraint (%s) has no matches.\n".formatted(" ", constraint.score().toShortString(), - constraint.constraintRef().constraintName())); - } else { - summary.append( - "%8s%s: constraint (%s) has %s matches:\n".formatted(" ", constraint.score().toShortString(), - constraint.constraintRef().constraintName(), matches.size())); - } - matches.stream() - .sorted(matchScoreComparator) - .limit(topLimit) - .forEach(match -> summary - .append("%12s%s: justified with (%s)\n".formatted(" ", - match.score().toShortString(), - match.justification()))); - - if (matches.size() > topLimit) { - summary.append("%12s... and %d more matches\n".formatted(" ", - matches.size() - topLimit)); - } - }); - - return summary.toString(); - } + String summarize(int topLimit); - @Override - public String toString() { - return "Score analysis of score %s with %d constraints.".formatted(score, constraintMap.size()); - } } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/package-info.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/package-info.java new file mode 100644 index 00000000000..09a82e9ed9d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains APIs to analyze the {@link ai.timefold.solver.core.api.score.Score} + * of a {@link ai.timefold.solver.core.api.domain.solution.PlanningSolution}. + * {@link ai.timefold.solver.core.api.score.analysis.ScoreAnalysis Score analysis} is a feature of Timefold Solver Enterprise + * Edition. + * These interfaces do not have any implementation in the open source version of Timefold Solver, + * and methods such as {@link ai.timefold.solver.core.api.solver.SolutionManager#analyze(java.lang.Object)} + * will throw an exception if called. + */ +package ai.timefold.solver.core.api.score.analysis; \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/AnalyzableIncrementalScoreCalculator.java b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/AnalyzableIncrementalScoreCalculator.java new file mode 100644 index 00000000000..81e8fd558ea --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/AnalyzableIncrementalScoreCalculator.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.api.score.calculator; + +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; + +import org.jspecify.annotations.NullMarked; + +/** + * Used for incremental java {@link Score} calculation with support for {@link ScoreAnalysis} + * Any implementation is naturally stateful. + *

+ * Note: Both incremental score calculation and score analysis are exclusive to Timefold Solver Enterprise Edition. + * They are not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw exceptions at runtime. + * + * @param the solution type, the class with the {@link PlanningSolution} annotation + * @param the score type to go with the solution + */ +@NullMarked +public interface AnalyzableIncrementalScoreCalculator> + extends IncrementalScoreCalculator { + + /** + * Tells this implementation whether constraint matching should be enabled or not. + * Will be called by the solver before the first call to {@link #resetWorkingSolution} + * and not again. + * + * @param constraintMatchRegistry use for registering constraint matches + */ + void enableConstraintMatch(ConstraintMatchRegistry constraintMatchRegistry); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchAwareIncrementalScoreCalculator.java b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchAwareIncrementalScoreCalculator.java deleted file mode 100644 index a7c2c3cd5ed..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchAwareIncrementalScoreCalculator.java +++ /dev/null @@ -1,54 +0,0 @@ -package ai.timefold.solver.core.api.score.calculator; - -import java.util.Collection; -import java.util.Map; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -/** - * Allows a {@link IncrementalScoreCalculator} to report {@link ConstraintMatchTotal}s - * for explaining a score (= which score constraints match for how much) - * and also for score corruption analysis. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @param the {@link Score} type - */ -@NullMarked -public interface ConstraintMatchAwareIncrementalScoreCalculator> - extends IncrementalScoreCalculator { - - /** - * Allows for increased performance because it only tracks if constraintMatchEnabled is true. - *

- * Every implementation should call {@link #resetWorkingSolution} - * and only handle the constraintMatchEnabled parameter specifically (or ignore it). - * - * @param workingSolution to pass to {@link #resetWorkingSolution}. - * @param constraintMatchEnabled true if {@link #getConstraintMatchTotals()} or {@link #getIndictmentMap()} might be called. - */ - void resetWorkingSolution(Solution_ workingSolution, boolean constraintMatchEnabled); - - /** - * @return never null; - * if a constraint is present in the problem but resulted in no matches, - * it should still be present with a {@link ConstraintMatchTotal#getConstraintMatchSet()} size of 0. - * @throws IllegalStateException if {@link #resetWorkingSolution}'s constraintMatchEnabled parameter was false - */ - Collection> getConstraintMatchTotals(); - - /** - * @return null if it should to be calculated non-incrementally from {@link #getConstraintMatchTotals()} - * @throws IllegalStateException if {@link #resetWorkingSolution}'s constraintMatchEnabled parameter was false - * @see ScoreExplanation#getIndictmentMap() - */ - @Nullable - Map> getIndictmentMap(); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchRegistration.java b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchRegistration.java new file mode 100644 index 00000000000..a00db21ad52 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchRegistration.java @@ -0,0 +1,44 @@ +package ai.timefold.solver.core.api.score.calculator; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; +import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; + +import org.jspecify.annotations.NullMarked; + +/** + * A registration for a constraint match, which can be cancelled. + *

+ * This is used by {@link IncrementalScoreCalculator} implementations to register constraint matches, + * which are later used for score justification and explanation. + *

+ * No two instances should ever be equal; + * if a constraint match is registered twice, two different instances will be counted. + * + * @param the score type to go with the solution + */ +@NullMarked +public interface ConstraintMatchRegistration> { + + ConstraintRef constraintRef(); + + Score_ score(); + + /** + * The justification of the match, which will be used in {@link ScoreAnalysis}. + * + * @return the justification of the match; the specific type depends on the user. + */ + ConstraintJustification justification(); + + /** + * Cancels the registration of this constraint match. + * Once canceled, the constraint match will no longer be counted for the purposes of score analysis. + * Once {@link IncrementalScoreCalculator#resetWorkingSolution(Object)} is called, + * all constraint matches registered with this instances' {@link ConstraintMatchRegistry} are automatically canceled + * and the user can throw away the references. + */ + void cancel(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchRegistry.java b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchRegistry.java new file mode 100644 index 00000000000..6aca84f3077 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/ConstraintMatchRegistry.java @@ -0,0 +1,54 @@ +package ai.timefold.solver.core.api.score.calculator; + +import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; +import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; + +import org.jspecify.annotations.NullMarked; + +/** + * + * @param + * @see AnalyzableIncrementalScoreCalculator Adding explainability to incremental score calculator. + */ +@NullMarked +public interface ConstraintMatchRegistry> { + + /** + * + * @param constraintRef identification of the constraint; obtain from {@link ConstraintRef#of(String)}. + * @param score full impact of the match; + * Any {@link ConstraintWeightOverrides} will be ignored. + * @param justification the justification of the match, which will be used in {@link ScoreAnalysis}; + * use {@link #registerConstraintMatch(ConstraintRef, Score_, Object...)} + * if you prefer to not provide a specific justification type. + * @throws IllegalStateException if constraint matching is not enabled + * @return the handler to cancel the match later if needed + */ + ConstraintMatchRegistration registerConstraintMatch(ConstraintRef constraintRef, Score_ score, + ConstraintJustification justification); + + /** + * + * @param constraintRef identification of the constraint; obtain from {@link ConstraintRef#of(String)}. + * @param score full impact of the match; + * Any {@link ConstraintWeightOverrides} will be ignored. + * @param justifications objects justifying the match, which will be used in {@link ScoreAnalysis}; + * these objects will be wrapped by a default internal {@link ConstraintJustification} implementation. + * @throws IllegalStateException if constraint matching is not enabled + * @return the handler to cancel the match later if needed + */ + default ConstraintMatchRegistration registerConstraintMatch(ConstraintRef constraintRef, Score_ score, + Object... justifications) { + return registerConstraintMatch(constraintRef, score, DefaultConstraintJustification.of(score, justifications)); + } + + /** + * @return the total score of all registered and non-canceled matches + */ + Score_ totalScore(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/IncrementalScoreCalculator.java b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/IncrementalScoreCalculator.java index cf7394f062c..c006e2ab4de 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/calculator/IncrementalScoreCalculator.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/calculator/IncrementalScoreCalculator.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import org.jspecify.annotations.NullMarked; @@ -12,29 +13,28 @@ * This is much faster than {@link EasyScoreCalculator} but requires much more code to implement too. *

* Any implementation is naturally stateful. + *

+ * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition. + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw exceptions at runtime. * * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the score type to go with the solution + * @see AnalyzableIncrementalScoreCalculator See incremental calculator with support for {@link ScoreAnalysis}. */ @NullMarked public interface IncrementalScoreCalculator> { /** - * There are no {@link #beforeEntityAdded(Object)} and {@link #afterEntityAdded(Object)} calls - * for entities that are already present in the workingSolution. + * Resets the internal caches and score to match the given working solution. + * It is recommended to build the internal caches lazily as the before/after events come in, + * as this method may be called several times in a row with the same working solution, + * and building the internal caches eagerly can be expensive. + * + * @param workingSolution the working solution to operate on */ void resetWorkingSolution(Solution_ workingSolution); - /** - * @param entity an instance of a {@link PlanningEntity} class - */ - void beforeEntityAdded(Object entity); - - /** - * @param entity an instance of a {@link PlanningEntity} class - */ - void afterEntityAdded(Object entity); - /** * @param entity an instance of a {@link PlanningEntity} class * @param variableName either a genuine or shadow {@link PlanningVariable} @@ -65,16 +65,6 @@ default void beforeListVariableChanged(Object entity, String variableName, int f default void afterListVariableChanged(Object entity, String variableName, int fromIndex, int toIndex) { } - /** - * @param entity an instance of a {@link PlanningEntity} class - */ - void beforeEntityRemoved(Object entity); - - /** - * @param entity an instance of a {@link PlanningEntity} class - */ - void afterEntityRemoved(Object entity); - /** * This method is only called if the {@link Score} cannot be predicted. * The {@link Score} can be predicted for example after an undo move. diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatchTotal.java deleted file mode 100644 index 4ef461336ea..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatchTotal.java +++ /dev/null @@ -1,48 +0,0 @@ -package ai.timefold.solver.core.api.score.constraint; - -import java.util.Set; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.domain.variable.PlanningVariable; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.solver.SolutionManager; - -import org.jspecify.annotations.NullMarked; - -/** - * Explains the {@link Score} of a {@link PlanningSolution}, from the opposite side than {@link Indictment}. - * Retrievable from {@link ScoreExplanation#getConstraintMatchTotalMap()}. - * - *

- * If possible, prefer using {@link SolutionManager#analyze(Object)} instead. - * - * @param the actual score type - */ -@NullMarked -public interface ConstraintMatchTotal> { - - ConstraintRef getConstraintRef(); - - /** - * The effective value of constraint weight after applying optional overrides. - * It is independent to the state of the {@link PlanningVariable planning variables}. - * Do not confuse with {@link #getScore()}. - */ - Score_ getConstraintWeight(); - - Set> getConstraintMatchSet(); - - /** - * @return {@code >= 0} - */ - default int getConstraintMatchCount() { - return getConstraintMatchSet().size(); - } - - /** - * Sum of the {@link #getConstraintMatchSet()}'s {@link ConstraintMatch#getScore()}. - */ - Score_ getScore(); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/Indictment.java b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/Indictment.java deleted file mode 100644 index 120e946ca03..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/Indictment.java +++ /dev/null @@ -1,73 +0,0 @@ -package ai.timefold.solver.core.api.score.constraint; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; - -import org.jspecify.annotations.NullMarked; - -/** - * Explains the {@link Score} of a {@link PlanningSolution}, from the opposite side than {@link ConstraintMatchTotal}. - * Retrievable from {@link ScoreExplanation#getIndictmentMap()}. - * - * @param the actual score type - */ -@NullMarked -public interface Indictment> { - - /** - * The object that was involved in causing the constraints to match. - * It is part of {@link ConstraintMatch#getIndictedObjectList()} of every {@link ConstraintMatch} - * returned by {@link #getConstraintMatchSet()}. - * - * @param Shorthand so that the user does not need to cast in user code. - */ - IndictedObject_ getIndictedObject(); - - Set> getConstraintMatchSet(); - - /** - * @return {@code >= 0} - */ - default int getConstraintMatchCount() { - return getConstraintMatchSet().size(); - } - - /** - * Retrieve {@link ConstraintJustification} instances associated with {@link ConstraintMatch}es in - * {@link #getConstraintMatchSet()}. - * This is equivalent to retrieving {@link #getConstraintMatchSet()} - * and collecting all {@link ConstraintMatch#getJustification()} objects into a list. - * - * @return guaranteed to contain unique instances - */ - List getJustificationList(); - - /** - * Retrieve {@link ConstraintJustification} instances associated with {@link ConstraintMatch}es in - * {@link #getConstraintMatchSet()}, which are of (or extend) a given constraint justification implementation. - * This is equivalent to retrieving {@link #getConstraintMatchSet()} - * and collecting all matching {@link ConstraintMatch#getJustification()} objects into a list. - * - * @return guaranteed to contain unique instances - */ - default List - getJustificationList(Class justificationClass) { - return getJustificationList() - .stream() - .filter(justification -> justificationClass.isAssignableFrom(justification.getClass())) - .map(j -> (ConstraintJustification_) j) - .collect(Collectors.toList()); - } - - /** - * Sum of the {@link #getConstraintMatchSet()}'s {@link ConstraintMatch#getScore()}. - */ - Score_ getScore(); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/package-info.java b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/package-info.java deleted file mode 100644 index b2fbf173b8d..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Explain a {@link ai.timefold.solver.core.api.score.Score} with - * {@link ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal} and - * {@link ai.timefold.solver.core.api.score.constraint.ConstraintMatch}. - */ -package ai.timefold.solver.core.api.score.constraint; diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/Constraint.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/Constraint.java index 1118b5abb22..87428051cdb 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/Constraint.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/Constraint.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.api.score.stream; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java index 79614ffc7cf..853fae9dd26 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.api.score.stream; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import org.jspecify.annotations.NullMarked; @@ -11,7 +11,7 @@ public interface ConstraintBuilder { * Builds a {@link Constraint} from the constraint stream. * The constraint will be placed in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}. * - * @param constraintName shows up in {@link ConstraintMatchTotal} during score justification + * @param constraintName shows up in {@link ScoreAnalysis} */ default Constraint asConstraint(String constraintName) { return asConstraintDescribed(constraintName, ""); @@ -21,7 +21,7 @@ default Constraint asConstraint(String constraintName) { * As defined by {@link #asConstraintDescribed(String, String, String)}, * placing the constraint in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}. * - * @param constraintName shows up in {@link ConstraintMatchTotal} during score justification + * @param constraintName shows up in {@link ScoreAnalysis} * @param constraintDescription can contain any character, but it is recommended to keep it short and concise. */ default Constraint asConstraintDescribed(String constraintName, String constraintDescription) { @@ -38,7 +38,7 @@ default Constraint asConstraintDescribed(String constraintName, String constrain * the constraint description is unlikely to be used externally as an identifier, * and therefore doesn't need to be URL-friendly, or protected against injection attacks. * - * @param constraintName shows up in {@link ConstraintMatchTotal} during score justification + * @param constraintName shows up in {@link ScoreAnalysis} * @param constraintDescription can contain any character, but it is recommended to keep it short and concise. * @param constraintGroup not used by the solver directly, but may be used by external tools to group constraints together, * such as by their source or by their purpose diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java index 9bb5e8b68f8..f3890812b45 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java @@ -5,7 +5,6 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; import ai.timefold.solver.core.api.solver.SolutionManager; @@ -43,7 +42,9 @@ * that the class(es) implementing this interface can be serialized into any format * which is supported by the {@link SolutionManager} implementation, typically JSON. * - * @see ConstraintMatch#getJustification() + *

+ * Note: {@link ScoreAnalysis} in general and constraint justifications in particular + * are exclusive to Timefold Solver Enterprise Edition. */ public interface ConstraintJustification { diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintMetaModel.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintMetaModel.java index d9a87e3f70d..ae7816bdc10 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintMetaModel.java @@ -3,8 +3,6 @@ import java.util.Collection; import java.util.Set; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; - import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintRef.java similarity index 78% rename from core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java rename to core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintRef.java index 024191c3ff4..fe13b131cd6 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintRef.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.api.score.constraint; +package ai.timefold.solver.core.api.score.stream; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintBuilder; @@ -7,8 +7,7 @@ /** * Represents a unique identifier of a constraint. *

- * Users should have no need to create instances of this record. - * If necessary, use {@link ConstraintRef#of(String)} and not the record's constructors. + * If you need an instance created, use {@link ConstraintRef#of(String)} and not the record's constructors. * * @param constraintName The constraint name. It must be unique. */ diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/DefaultConstraintJustification.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/DefaultConstraintJustification.java index c22d9f90dad..588b3790bcf 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/DefaultConstraintJustification.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/DefaultConstraintJustification.java @@ -6,40 +6,41 @@ import java.util.Objects; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; +import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** - * Default implementation of {@link ConstraintJustification}, returned by {@link ConstraintMatch#getJustification()} + * Default implementation of {@link ConstraintJustification}, + * exposed by {@link MatchAnalysis#justification() score analysis} * unless the user defined a custom justification mapping. */ +@NullMarked public final class DefaultConstraintJustification implements ConstraintJustification, Comparable { - public static @NonNull DefaultConstraintJustification of(Score impact, Object fact) { + public static DefaultConstraintJustification of(Score impact, Object fact) { return of(impact, Collections.singletonList(fact)); } - public static @NonNull DefaultConstraintJustification of(Score impact, Object factA, Object factB) { + public static DefaultConstraintJustification of(Score impact, Object factA, Object factB) { return of(impact, Arrays.asList(factA, factB)); } - public static @NonNull DefaultConstraintJustification of(Score impact, Object factA, Object factB, Object factC) { + public static DefaultConstraintJustification of(Score impact, Object factA, Object factB, Object factC) { return of(impact, Arrays.asList(factA, factB, factC)); } - public static @NonNull DefaultConstraintJustification of(Score impact, Object factA, Object factB, Object factC, - Object factD) { + public static DefaultConstraintJustification of(Score impact, Object factA, Object factB, Object factC, Object factD) { return of(impact, Arrays.asList(factA, factB, factC, factD)); } - public static @NonNull DefaultConstraintJustification of(Score impact, Object... facts) { + public static DefaultConstraintJustification of(Score impact, Object... facts) { return of(impact, Arrays.asList(facts)); } - public static @NonNull DefaultConstraintJustification of(Score impact, List facts) { + public static DefaultConstraintJustification of(Score impact, List facts) { return new DefaultConstraintJustification(impact, facts); } @@ -55,7 +56,7 @@ public > Score_ getImpact() { return (Score_) impact; } - public @NonNull List<@Nullable Object> getFacts() { + public List<@Nullable Object> getFacts() { return facts; } @@ -98,7 +99,7 @@ public int compareTo(DefaultConstraintJustification other) { return 0; } - private static int compareElements(Object left, Object right) { + private static int compareElements(@Nullable Object left, @Nullable Object right) { if (left == right) { return 0; } else if (left == null) { diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintBuilder.java index 1c05aeee2bd..f72ccac3b6b 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintBuilder.java @@ -1,13 +1,10 @@ package ai.timefold.solver.core.api.score.stream.bi; import java.util.Collection; -import java.util.function.BiFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintBuilder; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; @@ -21,29 +18,24 @@ * Unless {@link #justifyWith(TriFunction)} is called, * the default justification mapping will be used. * The function takes the input arguments and converts them into a {@link java.util.List}. - *

- * Unless {@link #indictWith(BiFunction)} is called, the default indicted objects' mapping will be used. - * The function takes the input arguments and converts them into a {@link java.util.List}. */ @NullMarked public interface BiConstraintBuilder> extends ConstraintBuilder { /** * Sets a custom function to apply on a constraint match to justify it. + * That function must not return a {@link Collection}, + * else {@link IllegalStateException} will be thrown during score calculation. + * + *

+ * Note: {@link ScoreAnalysis} in general and constraint justifications in particular + * are exclusive to Timefold Solver Enterprise Edition. + * Users of the open-source version of Timefold Solver can still use this method, + * but it will have no practical effect. * - * @see ConstraintMatch * @return this */ BiConstraintBuilder justifyWith( TriFunction justificationMapping); - /** - * Sets a custom function to mark any object returned by it as responsible for causing the constraint to match. - * Each object in the collection returned by this function will become an {@link Indictment} - * and be available as a key in {@link ScoreExplanation#getIndictmentMap()}. - * - * @return this - */ - BiConstraintBuilder indictWith(BiFunction> indictedObjectsMapping); - } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintBuilder.java index f5b8734b21c..7723bdb4cac 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintBuilder.java @@ -1,13 +1,11 @@ package ai.timefold.solver.core.api.score.stream.quad; import java.util.Collection; +import java.util.List; import ai.timefold.solver.core.api.function.PentaFunction; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintBuilder; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; @@ -19,31 +17,25 @@ * To build the constraint, use one of the terminal operations, such as {@link #asConstraint(String)}. *

* Unless {@link #justifyWith(PentaFunction)} is called, the default justification mapping will be used. - * The function takes the input arguments and converts them into a {@link java.util.List}. - *

- * Unless {@link #indictWith(QuadFunction)} is called, the default indicted objects' mapping will be used. - * The function takes the input arguments and converts them into a {@link java.util.List}. + * The function takes the input arguments and converts them into a {@link List}. */ @NullMarked public interface QuadConstraintBuilder> extends ConstraintBuilder { /** * Sets a custom function to apply on a constraint match to justify it. + * That function must not return a {@link Collection}, + * else {@link IllegalStateException} will be thrown during score calculation. + * + *

+ * Note: {@link ScoreAnalysis} in general and constraint justifications in particular + * are exclusive to Timefold Solver Enterprise Edition. + * Users of the open-source version of Timefold Solver can still use this method, + * but it will have no practical effect. * * @return this - * @see ConstraintMatch */ QuadConstraintBuilder justifyWith( PentaFunction justificationMapping); - /** - * Sets a custom function to mark any object returned by it as responsible for causing the constraint to match. - * Each object in the collection returned by this function will become an {@link Indictment} - * and be available as a key in {@link ScoreExplanation#getIndictmentMap()}. - * - * @return this - */ - QuadConstraintBuilder indictWith( - QuadFunction> indictedObjectsMapping); - } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/test/SingleConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/test/SingleConstraintAssertion.java index f67a8d27096..db412a25dc3 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/test/SingleConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/test/SingleConstraintAssertion.java @@ -56,47 +56,6 @@ SingleConstraintAssertion justifiesWithExactly(@Nullable String message, return justifiesWithExactly(null, justifications); } - /** - * Asserts that the {@link Constraint} being tested, given a set of facts, results in the given indictments. - * - * @param indictments the expected indictments. - * @throws AssertionError when the expected penalty is not observed - */ - default @NonNull SingleConstraintAssertion indictsWith(@NonNull Object @NonNull... indictments) { - return indictsWith(null, indictments); - } - - /** - * As defined by {@link #indictsWith(Object...)}. - * - * @param message description of the scenario being asserted - * @param indictments the expected indictments. - * @throws AssertionError when the expected penalty is not observed - */ - @NonNull - SingleConstraintAssertion indictsWith(@Nullable String message, @NonNull Object @NonNull... indictments); - - /** - * Asserts that the {@link Constraint} being tested, given a set of facts, results in the given indictments and - * nothing else. - * - * @param indictments the expected indictments. - * @throws AssertionError when the expected penalty is not observed - */ - default @NonNull SingleConstraintAssertion indictsWithExactly(@NonNull Object @NonNull... indictments) { - return indictsWithExactly(null, indictments); - } - - /** - * As defined by {@link #indictsWithExactly(Object...)}. - * - * @param message description of the scenario being asserted - * @param indictments the expected indictments. - * @throws AssertionError when the expected penalty is not observed - */ - @NonNull - SingleConstraintAssertion indictsWithExactly(@Nullable String message, @NonNull Object @NonNull... indictments); - /** * Asserts that the {@link Constraint} being tested, given a set of facts, results in no impact on the score. *

diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintBuilder.java index 64783749a79..24c26c73ab3 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintBuilder.java @@ -1,13 +1,11 @@ package ai.timefold.solver.core.api.score.stream.tri; import java.util.Collection; +import java.util.List; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintBuilder; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; @@ -19,30 +17,25 @@ * To build the constraint, use one of the terminal operations, such as {@link #asConstraint(String)}. *

* Unless {@link #justifyWith(QuadFunction)} is called, the default justification mapping will be used. - * The function takes the input arguments and converts them into a {@link java.util.List}. - *

- * Unless {@link #indictWith(TriFunction)} is called, the default indicted objects' mapping will be used. - * The function takes the input arguments and converts them into a {@link java.util.List}. + * The function takes the input arguments and converts them into a {@link List}. */ @NullMarked public interface TriConstraintBuilder> extends ConstraintBuilder { /** * Sets a custom function to apply on a constraint match to justify it. + * That function must not return a {@link Collection}, + * else {@link IllegalStateException} will be thrown during score calculation. + * + *

+ * Note: {@link ScoreAnalysis} in general and constraint justifications in particular + * are exclusive to Timefold Solver Enterprise Edition. + * Users of the open-source version of Timefold Solver can still use this method, + * but it will have no practical effect. * - * @see ConstraintMatch * @return this */ TriConstraintBuilder justifyWith( QuadFunction justificationMapping); - /** - * Sets a custom function to mark any object returned by it as responsible for causing the constraint to match. - * Each object in the collection returned by this function will become an {@link Indictment} - * and be available as a key in {@link ScoreExplanation#getIndictmentMap()}. - * - * @return this - */ - TriConstraintBuilder indictWith(TriFunction> indictedObjectsMapping); - } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintBuilder.java index 557fabab18a..06123229d44 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintBuilder.java @@ -2,12 +2,9 @@ import java.util.Collection; import java.util.function.BiFunction; -import java.util.function.Function; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintBuilder; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; @@ -20,31 +17,24 @@ *

* Unless {@link #justifyWith(BiFunction)} is called, the default justification mapping will be used. * The function takes the input arguments and converts them into a {@link java.util.List}. - *

- * Unless {@link #indictWith(Function)} is called, the default indicted objects' mapping will be used. - * The function takes the input arguments and converts them into a {@link java.util.List}. */ @NullMarked public interface UniConstraintBuilder> extends ConstraintBuilder { /** * Sets a custom function to apply on a constraint match to justify it. - * That function must not return a {@link java.util.Collection}, + * That function must not return a {@link Collection}, * else {@link IllegalStateException} will be thrown during score calculation. * + *

+ * Note: {@link ScoreAnalysis} in general and constraint justifications in particular + * are exclusive to Timefold Solver Enterprise Edition. + * Users of the open-source version of Timefold Solver can still use this method, + * but it will have no practical effect. + * * @return this - * @see ConstraintMatch */ UniConstraintBuilder justifyWith( BiFunction justificationMapping); - /** - * Sets a custom function to mark any object returned by it as responsible for causing the constraint to match. - * Each object in the collection returned by this function will become an {@link Indictment} - * and be available as a key in {@link ScoreExplanation#getIndictmentMap()}. - * - * @return this - */ - UniConstraintBuilder indictWith(Function> indictedObjectsMapping); - } diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java index 106e660b3f2..0c4fc2b71fc 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java @@ -10,12 +10,10 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.impl.domain.variable.ShadowVariableUpdateHelper; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.solver.DefaultSolutionManager; import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; @@ -23,12 +21,14 @@ import org.jspecify.annotations.Nullable; /** - * A stateless service to help calculate {@link Score}, {@link ConstraintMatchTotal}, - * {@link Indictment}, etc. + * A stateless service to help calculate {@link Score}, {@link ConstraintMatchTotal}, etc. *

* To create a {@link SolutionManager} instance, use {@link #create(SolverFactory)}. *

* These methods are thread-safe unless explicitly stated otherwise. + *

+ * Some of these methods require Timefold Solver Enterprise Edition. + * All of them are clearly marked as such in their respective Javadocs. * * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the actual score type @@ -116,26 +116,6 @@ static void updateShadowVariables(Solution_ solution) { ShadowVariableUpdateHelper. create().updateShadowVariables(solution); } - /** - * As defined by {@link #explain(Object, SolutionUpdatePolicy)}, - * using {@link SolutionUpdatePolicy#UPDATE_ALL}. - */ - default ScoreExplanation explain(Solution_ solution) { - return explain(solution, UPDATE_ALL); - } - - /** - * Calculates and retrieves {@link ConstraintMatchTotal}s and {@link Indictment}s necessary for describing the - * quality of a particular solution. - * For a simplified, faster and JSON-friendly alternative, see {@link #analyze(Object)}}. - * - * @param solutionUpdatePolicy if unsure, pick {@link SolutionUpdatePolicy#UPDATE_ALL} - * @throws IllegalStateException when constraint matching is disabled or not supported by the underlying score - * calculator, such as {@link EasyScoreCalculator}. - * @see SolutionUpdatePolicy Description of individual policies with respect to performance trade-offs. - */ - ScoreExplanation explain(Solution_ solution, SolutionUpdatePolicy solutionUpdatePolicy); - /** * As defined by {@link #analyze(Object, ScoreAnalysisFetchPolicy, SolutionUpdatePolicy)}, * using {@link SolutionUpdatePolicy#UPDATE_ALL} and {@link ScoreAnalysisFetchPolicy#FETCH_ALL}. @@ -154,7 +134,9 @@ default ScoreAnalysis analyze(Solution_ solution, ScoreAnalysisFetchPoli /** * Calculates and retrieves information about which constraints contributed to the solution's score. - * This is a faster, JSON-friendly version of {@link #explain(Object)}. + *

+ * Note: {@link ScoreAnalysis Score analysis} is exclusive to Timefold Solver Enterprise Edition. + * This method will throw an exception if the binaries can't be found. * * @param solution must be fully initialized otherwise an exception is thrown * @param fetchPolicy if unsure, pick {@link ScoreAnalysisFetchPolicy#FETCH_MATCH_COUNT} @@ -181,7 +163,10 @@ ScoreAnalysis analyze(Solution_ solution, ScoreAnalysisFetchPolicy fetch * There are no guarantees for backward compatibility; * its signature or the types it operates on and returns may change or be removed without prior notice, * although we will strive to avoid this as much as possible. - * + *

+ * Note: Solution diff is exclusive to Timefold Solver Enterprise Edition. + * This method will throw an exception if the binaries can't be found. + * * @param oldSolution The solution to use as a base for comparison. * @param newSolution The solution to compare against the base. * @return A diff object containing information about the differences between the two solutions. @@ -282,6 +267,9 @@ default List + * Note: Recommendations are exclusive to Timefold Solver Enterprise Edition. + * This method will throw an exception if the binaries can't be found. * * @param solution for basic variable, must be fully initialized or have a single entity unassigned. * For list variable, all values must be assigned to some list, with the optional exception of one. diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionUpdatePolicy.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionUpdatePolicy.java index 8c92f72303f..6cb31c1321b 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionUpdatePolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionUpdatePolicy.java @@ -49,8 +49,7 @@ public enum SolutionUpdatePolicy { UPDATE_SHADOW_VARIABLES_ONLY(false, true), /** * Does not run anything. - * Improves performance during {@link SolutionManager#analyze(Object, ScoreAnalysisFetchPolicy, SolutionUpdatePolicy)} - * and {@link SolutionManager#explain(Object, SolutionUpdatePolicy)}, + * Improves performance during {@link SolutionManager#analyze(Object, ScoreAnalysisFetchPolicy, SolutionUpdatePolicy)}, * where the user can guarantee that the solution is already up to date. * Otherwise serves no purpose. */ diff --git a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java index cf6f18a762d..abb78182010 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java @@ -108,19 +108,39 @@ public void setConstraintStreamProfilingEnabled(Boolean constraintStreamProfilin this.constraintStreamProfilingEnabled = constraintStreamProfilingEnabled; } + /** + * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw an exception at runtime. + */ public @Nullable Class getIncrementalScoreCalculatorClass() { return incrementalScoreCalculatorClass; } + /** + * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw an exception at runtime. + */ public void setIncrementalScoreCalculatorClass( @Nullable Class incrementalScoreCalculatorClass) { this.incrementalScoreCalculatorClass = incrementalScoreCalculatorClass; } + /** + * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw an exception at runtime. + */ public @Nullable Map<@NonNull String, @NonNull String> getIncrementalScoreCalculatorCustomProperties() { return incrementalScoreCalculatorCustomProperties; } + /** + * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw an exception at runtime. + */ public void setIncrementalScoreCalculatorCustomProperties( @Nullable Map<@NonNull String, @NonNull String> incrementalScoreCalculatorCustomProperties) { this.incrementalScoreCalculatorCustomProperties = incrementalScoreCalculatorCustomProperties; @@ -184,6 +204,11 @@ public void setAssertionScoreDirectorFactory(@Nullable ScoreDirectorFactoryConfi return this; } + /** + * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw an exception at runtime. + */ public @NonNull ScoreDirectorFactoryConfig withIncrementalScoreCalculatorClass( @NonNull Class incrementalScoreCalculatorClass) { @@ -191,6 +216,11 @@ public void setAssertionScoreDirectorFactory(@Nullable ScoreDirectorFactoryConfi return this; } + /** + * Note: Incremental score calculation is exclusive to Timefold Solver Enterprise Edition + * It is not available in the open-source version of Timefold Solver, + * and attempts to use it without a valid license will throw an exception at runtime. + */ public @NonNull ScoreDirectorFactoryConfig withIncrementalScoreCalculatorCustomProperties( @NonNull Map<@NonNull String, @NonNull String> incrementalScoreCalculatorCustomProperties) { diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index e5973025e9a..6743fde9624 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -1,11 +1,18 @@ package ai.timefold.solver.core.enterprise; import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.api.solver.RecommendedAssignment; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; @@ -17,11 +24,13 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListMultistageMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; import ai.timefold.solver.core.config.partitionedsearch.PartitionedSearchPhaseConfig; +import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.bavet.common.InnerConstraintProfiler; import ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider; import ai.timefold.solver.core.impl.constructionheuristic.decider.forager.ConstructionHeuristicForager; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.declarative.TopologicalOrderGraph; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; @@ -36,8 +45,17 @@ import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; import ai.timefold.solver.core.impl.neighborhood.MoveRepository; import ai.timefold.solver.core.impl.partitionedsearch.PartitionedSearchPhase; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; +import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; +import ai.timefold.solver.core.impl.score.director.InnerScore; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; import ai.timefold.solver.core.impl.solver.termination.SolverTermination; +import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel; +import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; + +import org.jspecify.annotations.Nullable; public interface TimefoldSolverEnterpriseService { @@ -49,10 +67,9 @@ final class InstanceCarrier { } - String SOLVER_NAME = "Timefold Solver"; - String COMMUNITY_NAME = "Community Edition"; + String COMMUNITY_NAME = "Timefold Solver Community Edition"; String COMMUNITY_COORDINATES = "ai.timefold.solver:timefold-solver-core"; - String ENTERPRISE_NAME = "Enterprise Edition"; + String ENTERPRISE_NAME = "Timefold Solver Enterprise Edition"; String ENTERPRISE_COORDINATES = "ai.timefold.solver.enterprise:timefold-solver-enterprise-core"; String DEVELOPMENT_SNAPSHOT = "Development Snapshot"; @@ -99,14 +116,21 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { No valid Timefold Enterprise License was found. Please contact Timefold to obtain a valid license, or if you believe that this message was given in error.""", cause); + } catch (EnterpriseProductException cause) { + throw new IllegalStateException(""" + Valid Timefold Enterprise License was found, but it does not entitle you to run "%s". + Maybe %s. + Please contact Timefold to obtain an applicable license, + or if you believe that this message was given in error.""" + .formatted(feature.getName(), feature.getWorkaround()), cause); } catch (Exception cause) { throw new IllegalStateException(""" - %s requested but %s %s could not be loaded. + A feature of Enterprise Edition "%s" was requested but it could not be loaded. Maybe add the %s dependency, or %s. - Note: %s %s is a commercial product. - Visit https://timefold.ai to find out more, or contact Timefold customer support.""".formatted( - feature.getName(), SOLVER_NAME, ENTERPRISE_NAME, feature.getWorkaround(), ENTERPRISE_COORDINATES, - SOLVER_NAME, ENTERPRISE_NAME), cause); + Please contact Timefold to obtain an applicable license, + or if you believe that this message was given in error.""" + .formatted(feature.getName(), ENTERPRISE_COORDINATES, feature.getWorkaround()), + cause); } } @@ -118,6 +142,14 @@ static T loadOrDefault(Function builder, } } + static @Nullable T loadOrNull(Function builder) { + try { + return builder.apply(load()); + } catch (Exception e) { + return null; + } + } + TopologicalOrderGraph buildTopologyGraph(int size); /** @@ -165,6 +197,22 @@ DestinationSelector applyNearbySelection(DestinationSelec InnerConstraintProfiler buildConstraintProfiler(); + > AbstractScoreDirectorFactory + buildIncrementalScoreDirectorFactory(ScoreDirectorFactoryConfig config, + SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode); + + > ScoreAnalysis analyze(InnerScore state, + Map> constraintMatchTotalMap, ScoreAnalysisFetchPolicy fetchPolicy); + + PlanningSolutionDiff solutionDiff(PlanningSolutionMetaModel metaModel, + Solution_ oldSolution, Solution_ newSolution); + + , In_, Out_> + Function, List>> buildRecommender( + DefaultSolverFactory solverFactory, Solution_ solution, In_ evaluatedEntityOrElement, + Function propositionFunction, + ScoreAnalysisFetchPolicy fetchPolicy); + enum Feature { MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"), PARTITIONED_SEARCH("Partitioned search", "remove partitioned search phase from solver configuration"), @@ -172,7 +220,11 @@ enum Feature { AUTOMATIC_NODE_SHARING("Automatic node sharing", "remove automatic node sharing from solver configuration"), MULTISTAGE_MOVE("Multistage move selector", "remove multistageMoveSelector and/or listMultistageMoveSelector from the solver configuration"), - CONSTRAINT_PROFILING("Constraint profiling", "remove constraintStreamProfilingEnabled from the solver configuration"); + CONSTRAINT_PROFILING("Constraint profiling", "remove constraintStreamProfilingEnabled from the solver configuration"), + SCORE_ANALYSIS("Score analysis", "do not use SolutionManager's analyze() method"), + RECOMMENDATIONS("Recommendations", "do not use SolutionManager's recommendAssignment() method"), + INCREMENTAL_SCORE_CALCULATOR("Incremental score calculator", + "remove incrementalScoreCalculatorClass and incrementalScoreCalculatorCustomProperties from the solver configuration"); private final String name; private final String workaround; @@ -210,4 +262,16 @@ public EnterpriseLicenseException(String message, Throwable cause) { } + final class EnterpriseProductException extends RuntimeException { + + public EnterpriseProductException(String message) { + super(message); + } + + public EnterpriseProductException(String message, Throwable cause) { + super(message, cause); + } + + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java index 2a02b0bce05..e8a81af31d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java @@ -12,9 +12,9 @@ import java.util.stream.Stream; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintStream; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintStream; @@ -106,16 +106,14 @@ public boolean guaranteesDistinct() { protected > Constraint buildConstraint(String constraintName, String description, String constraintGroup, Score_ constraintWeight, ScoreImpactType impactType, Object justificationFunction, - Object indictedObjectsMapping, BavetScoringConstraintStream stream) { + BavetScoringConstraintStream stream) { var resolvedJustificationMapping = Objects.requireNonNullElseGet(justificationFunction, this::getDefaultJustificationMapping); - var resolvedIndictedObjectsMapping = - Objects.requireNonNullElseGet(indictedObjectsMapping, this::getDefaultIndictedObjectsMapping); var isConstraintWeightConfigurable = constraintWeight == null; var constraintRef = ConstraintRef.of(constraintName); var constraint = new BavetConstraint<>(constraintFactory, constraintRef, description, constraintGroup, isConstraintWeightConfigurable ? null : constraintWeight, impactType, resolvedJustificationMapping, - resolvedIndictedObjectsMapping, stream); + stream); stream.setConstraint(constraint); return constraint; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/InnerConstraintProfiler.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/InnerConstraintProfiler.java index 4ff4d75caa2..a3f33c5592b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/InnerConstraintProfiler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/InnerConstraintProfiler.java @@ -2,7 +2,7 @@ import java.util.Set; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java deleted file mode 100644 index 175f977ca0d..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightSupplier.java +++ /dev/null @@ -1,49 +0,0 @@ -package ai.timefold.solver.core.impl.domain.solution; - -import java.util.Set; - -import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.ConstraintProvider; -import ai.timefold.solver.core.impl.domain.common.DomainAccessType; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; - -// TODO remove this indirection now that OverridesBasedConstraintWeightSupplier is the only implementation -public sealed interface ConstraintWeightSupplier> - permits OverridesBasedConstraintWeightSupplier { - - void initialize(SolutionDescriptor solutionDescriptor, MemberAccessorFactory memberAccessorFactory, - DomainAccessType domainAccessType); - - /** - * Will be called after {@link #initialize(SolutionDescriptor, MemberAccessorFactory, DomainAccessType)}. - * Has the option of failing fast in case of discrepancies - * between the constraints defined in {@link ConstraintProvider} - * and the constraints defined in the configuration. - * - * @param userDefinedConstraints never null - */ - void validate(Solution_ workingSolution, Set userDefinedConstraints); - - /** - * The class that carries the constraint weights. - * It will be {@link ConstraintWeightOverrides}. - * - * @return never null - */ - Class getProblemFactClass(); - - /** - * Get the weight for the constraint if known to the supplier. - * Supplies may choose not to provide a value for unknown constraints, - * which is the case for {@link OverridesBasedConstraintWeightSupplier}. - * - * @param constraintRef never null - * @param workingSolution never null - * @return may be null, if the provider does not know the constraint - */ - Score_ getConstraintWeight(ConstraintRef constraintRef, Solution_ workingSolution); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/ConstraintWeightSupplier.java similarity index 60% rename from core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java rename to core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/ConstraintWeightSupplier.java index f42907e6b06..59994930f1b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/OverridesBasedConstraintWeightSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/ConstraintWeightSupplier.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.domain.solution; +package ai.timefold.solver.core.impl.domain.solution.descriptor; import java.lang.reflect.Field; import java.lang.reflect.Member; @@ -8,23 +8,23 @@ import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; -import ai.timefold.solver.core.impl.domain.common.DomainAccessType; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; -public final class OverridesBasedConstraintWeightSupplier, Solution_> - implements ConstraintWeightSupplier { +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; - public static > ConstraintWeightSupplier create( - SolutionDescriptor solutionDescriptor, DescriptorPolicy descriptorPolicy, - Field field) { +@NullMarked +public final class ConstraintWeightSupplier> { + + @SuppressWarnings("unchecked") + public static > ConstraintWeightSupplier + create(SolutionDescriptor solutionDescriptor, DescriptorPolicy descriptorPolicy, Field field) { var method = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName()); Class> overridesClass; Member member; @@ -38,47 +38,44 @@ public static > ConstraintWeightSupplier overridesClass = (Class>) method.getReturnType(); } var memberAccessor = descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(member, - MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, - descriptorPolicy.getDomainAccessType()); - return new OverridesBasedConstraintWeightSupplier<>(solutionDescriptor, memberAccessor, overridesClass); + MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER, descriptorPolicy.getDomainAccessType()); + return new ConstraintWeightSupplier<>(solutionDescriptor, memberAccessor, overridesClass); } private final SolutionDescriptor solutionDescriptor; private final MemberAccessor overridesAccessor; private final Class> overridesClass; - private OverridesBasedConstraintWeightSupplier(SolutionDescriptor solutionDescriptor, - MemberAccessor overridesAccessor, Class> overridesClass) { + private ConstraintWeightSupplier(SolutionDescriptor solutionDescriptor, MemberAccessor overridesAccessor, + Class> overridesClass) { this.solutionDescriptor = Objects.requireNonNull(solutionDescriptor); this.overridesAccessor = Objects.requireNonNull(overridesAccessor); this.overridesClass = Objects.requireNonNull(overridesClass); } - @Override - public void initialize(SolutionDescriptor solutionDescriptor, MemberAccessorFactory memberAccessorFactory, - DomainAccessType domainAccessType) { - // No need to do anything. - } - - @Override - public void validate(Solution_ workingSolution, Set userDefinedConstraints) { - var userDefinedConstraintNames = userDefinedConstraints.stream() - .map(ConstraintRef::constraintName) - .collect(Collectors.toSet()); + /** + * Has the option of failing fast in case of discrepancies + * between the constraints defined in {@link ConstraintProvider} + * and the constraints defined in the configuration. + * + * @param workingSolution may be null, in which case the supplier will use the default constraint weights + * @param userDefinedConstraints never null + */ + public void validate(@Nullable Solution_ workingSolution, Set userDefinedConstraints) { + var userDefinedConstraintNames = + userDefinedConstraints.stream().map(ConstraintRef::constraintName).collect(Collectors.toSet()); // Constraint verifier is known to cause null here. var overrides = workingSolution == null ? ConstraintWeightOverrides.none() : Objects.requireNonNull(getConstraintWeights(workingSolution)); var supportedConstraints = overrides.getKnownConstraintNames(); var excessiveConstraints = supportedConstraints.stream() - .filter(constraintName -> !userDefinedConstraintNames.contains(constraintName)) - .collect(Collectors.toSet()); + .filter(constraintName -> !userDefinedConstraintNames.contains(constraintName)).collect(Collectors.toSet()); if (!excessiveConstraints.isEmpty()) { throw new IllegalStateException(""" The constraint weight overrides contain the following constraints (%s) \ that are not in the user-defined constraints (%s). - Maybe check your %s for missing constraints.""" - .formatted(excessiveConstraints, userDefinedConstraintNames, - ConstraintProvider.class.getSimpleName())); + Maybe check your %s for missing constraints.""".formatted(excessiveConstraints, userDefinedConstraintNames, + ConstraintProvider.class.getSimpleName())); } // Constraints are allowed to be missing; the default value provided by the ConstraintProvider will be used. } @@ -88,13 +85,25 @@ private ConstraintWeightOverrides getConstraintWeights(Solution_ working return (ConstraintWeightOverrides) overridesAccessor.executeGetter(workingSolution); } - @Override - public Class getProblemFactClass() { + /** + * The class that carries the constraint weights. + * + * @return never null + */ + public Class> getProblemFactClass() { return overridesClass; } - @Override - public Score_ getConstraintWeight(ConstraintRef constraintRef, Solution_ workingSolution) { + /** + * Get the weight for the constraint if known to the supplier. + * Supplies may choose not to provide a value for unknown constraints, + * which is the case for {@link ConstraintWeightSupplier}. + * + * @param constraintRef never null + * @param workingSolution if null, will return null + * @return may be null, if the provider does not know the constraint + */ + public @Nullable Score_ getConstraintWeight(ConstraintRef constraintRef, @Nullable Solution_ workingSolution) { if (workingSolution == null) { // ConstraintVerifier is known to cause null here. return null; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningEntityDiff.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningEntityDiff.java deleted file mode 100644 index 2aa850f8784..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningEntityDiff.java +++ /dev/null @@ -1,98 +0,0 @@ -package ai.timefold.solver.core.impl.domain.solution.descriptor; - -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningEntityMetaModel; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -record DefaultPlanningEntityDiff(PlanningSolutionDiff solutionDiff, Entity_ entity, - PlanningEntityMetaModel entityMetaModel, - Map> variableDiffMap) - implements - PlanningEntityDiff { - - @SuppressWarnings("unchecked") - DefaultPlanningEntityDiff(PlanningSolutionDiff solutionDiff, Entity_ entity) { - this(solutionDiff, entity, - (PlanningEntityMetaModel) solutionDiff.solutionMetaModel().entity(entity.getClass())); - } - - DefaultPlanningEntityDiff(PlanningSolutionDiff solutionDiff, Entity_ entity, - PlanningEntityMetaModel entityMetaModel) { - this(solutionDiff, entity, entityMetaModel, new LinkedHashMap<>(entityMetaModel.variables().size())); - } - - @SuppressWarnings("unchecked") - @Override - public PlanningVariableDiff variableDiff() { - if (variableDiffMap.size() != 1) { - throw new IllegalStateException(""" - There is more than one planning variable (%s) on the planning entity (%s). - Use variableDiff(String) instead.""".formatted(variableDiffMap.keySet(), entity.getClass())); - } - return (PlanningVariableDiff) variableDiffs().iterator().next(); - } - - void addVariableDiff(PlanningVariableDiff variableDiff) { - variableDiffMap.put(variableDiff.variableMetaModel().name(), variableDiff); - } - - @SuppressWarnings("unchecked") - @Override - public @Nullable PlanningVariableDiff variableDiff(String variableRef) { - return (PlanningVariableDiff) variableDiffMap.get(variableRef); - } - - @Override - public Collection> variableDiffs() { - return Collections.unmodifiableCollection(variableDiffMap.values()); - } - - @Override - public String toString() { - return """ - Difference between two solutions in variable(s) of planning entity (%s) of type (%s): - %s - """.formatted(entity, entityMetaModel().type(), entityDiffToString(this)); - } - - private static String entityDiffToString(PlanningEntityDiff entityDiff) { - var variableDiffs = entityDiff.variableDiffs(); - if (variableDiffs.size() == 1) { - var diff = variableDiffs.iterator().next(); - return " %s: %s -> %s".formatted(diff.variableMetaModel().name(), diff.oldValue(), diff.newValue()); - } - return variableDiffs.stream() - .map(diff -> " %s (%s): %s -> %s".formatted(diff.variableMetaModel().name(), - diff.variableMetaModel() instanceof GenuineVariableMetaModel ? "genuine" : "shadow", - diff.oldValue(), diff.newValue())) - .collect(Collectors.joining(System.lineSeparator())); - } - - @Override - public boolean equals(@Nullable Object o) { - if (!(o instanceof DefaultPlanningEntityDiff that)) { - return false; - } - return Objects.equals(entityMetaModel, that.entityMetaModel) - && Objects.equals(solutionDiff, that.solutionDiff) - && Objects.equals(entity, that.entity); - } - - @Override - public int hashCode() { - return Objects.hash(entityMetaModel, solutionDiff, entity); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningSolutionDiff.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningSolutionDiff.java deleted file mode 100644 index a7f268880c7..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningSolutionDiff.java +++ /dev/null @@ -1,177 +0,0 @@ -package ai.timefold.solver.core.impl.domain.solution.descriptor; - -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -final class DefaultPlanningSolutionDiff implements PlanningSolutionDiff { - - private static final int TO_STRING_ITEM_LIMIT = 5; - - private final PlanningSolutionMetaModel solutionMetaModel; - private final Solution_ oldSolution; - private final Solution_ newSolution; - private final Set removedEntities; - private final Set addedEntities; - private final Map, Set>> entityDiffMap = new LinkedHashMap<>(); - - DefaultPlanningSolutionDiff(PlanningSolutionMetaModel solutionMetaModel, Solution_ oldSolution, - Solution_ newSolution, Set removedEntities, Set addedEntities) { - this.solutionMetaModel = Objects.requireNonNull(solutionMetaModel); - this.oldSolution = Objects.requireNonNull(oldSolution); - this.newSolution = Objects.requireNonNull(newSolution); - this.removedEntities = Collections.unmodifiableSet(Objects.requireNonNull(removedEntities)); - this.addedEntities = Collections.unmodifiableSet(Objects.requireNonNull(addedEntities)); - } - - @Override - public PlanningSolutionMetaModel solutionMetaModel() { - return solutionMetaModel; - } - - @SuppressWarnings("unchecked") - @Override - public @Nullable PlanningEntityDiff entityDiff(Entity_ entity) { - return entityDiffs((Class) entity.getClass()).stream() - .filter(entityDiff -> entityDiff.entity().equals(entity)) - .findFirst() - .orElse(null); - } - - @Override - public Set> entityDiffs() { - if (entityDiffMap.isEmpty()) { - return Collections.emptySet(); - } - return entityDiffMap.values().stream() - .flatMap(Set::stream) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Set> entityDiffs(Class entityClass) { - var entityDiffSet = (Set) entityDiffMap.get(entityClass); - if (entityDiffSet == null) { - return Collections.emptySet(); - } - return Collections.unmodifiableSet(entityDiffSet); - } - - void addEntityDiff(PlanningEntityDiff entityDiff) { - entityDiffMap.computeIfAbsent(entityDiff.entityMetaModel().type(), k -> new LinkedHashSet<>()) - .add(entityDiff); - } - - @Override - public Solution_ oldSolution() { - return oldSolution; - } - - @Override - public Solution_ newSolution() { - return newSolution; - } - - @Override - public Set removedEntities() { - return removedEntities; - } - - @Override - public Set addedEntities() { - return addedEntities; - } - - @Override - public String toString() { - return toString(TO_STRING_ITEM_LIMIT); - } - - /** - * Like {@link #toString()}, but with a configurable limit on the number of items listed. - * - * @param limit Use Integer.MAX_VALUE to list all items. - * @return A string representation of the diff. - */ - String toString(int limit) { - var removedEntityString = collectionToString(removedEntities, limit); - var addedEntityString = collectionToString(addedEntities, limit); - var entityDiffStringList = entityDiffMap.values().stream() - .flatMap(Set::stream) - .map(DefaultPlanningSolutionDiff::entityDiffToString) - .toList(); - var entityDiffString = collectionToString(entityDiffStringList, limit); - return """ - Difference(s) between old planning solution (%s) and new planning solution (%s): - - - Old solution entities not present in new solution: - %s - - - New solution entities not present in old solution: - %s - - - Entities changed between the solutions: - %s - """.formatted(oldSolution, newSolution, removedEntityString, addedEntityString, entityDiffString); - } - - private static String collectionToString(Collection entitySet, int limit) { - if (entitySet.isEmpty()) { - return " (None.)"; - } - var addedEntityString = entitySet.stream() - .limit(limit) - .map(s -> " " + s.toString()) - .collect(Collectors.joining(System.lineSeparator())); - if (entitySet.size() > limit) { - addedEntityString += System.lineSeparator() + " ..."; - } - return addedEntityString; - } - - static String entityDiffToString(PlanningEntityDiff entityDiff) { - var entity = entityDiff.entity(); - var variableDiffs = entityDiff.variableDiffs(); - if (entityDiff.entityMetaModel().variables().size() == 1) { - var variableDiff = variableDiffs.iterator().next(); - return "%s (%s -> %s)".formatted(entity, variableDiff.oldValue(), variableDiff.newValue()); - } - var variableDiffString = variableDiffs.stream() - .map(diff -> " %s (%s): %s -> %s".formatted(diff.variableMetaModel().name(), - diff.variableMetaModel() instanceof GenuineVariableMetaModel ? "genuine" : "shadow", - diff.oldValue(), diff.newValue())) - .collect(Collectors.joining(System.lineSeparator())); - return """ - %s: - %s""".formatted(entity, variableDiffString); - } - - @Override - public boolean equals(@Nullable Object o) { - if (!(o instanceof DefaultPlanningSolutionDiff that)) { - return false; - } - return Objects.equals(solutionMetaModel, that.solutionMetaModel) && Objects.equals(oldSolution, that.oldSolution) - && Objects.equals(newSolution, that.newSolution); - } - - @Override - public int hashCode() { - return Objects.hash(solutionMetaModel, oldSolution, newSolution); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableDiff.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableDiff.java deleted file mode 100644 index ca56356d2a4..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableDiff.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.domain.solution.descriptor; - -import java.util.Objects; - -import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -record DefaultPlanningVariableDiff(PlanningEntityDiff entityDiff, - VariableMetaModel variableMetaModel, Value_ oldValue, Value_ newValue) - implements - PlanningVariableDiff { - - DefaultPlanningVariableDiff { - Objects.requireNonNull(entityDiff); - Objects.requireNonNull(variableMetaModel); - } - - @Override - public String toString() { - return """ - Difference between two solutions in a %s planning variable (%s) of a planning entity (%s) of type (%s): - Old value: %s - New value: %s - """ - .formatted( - variableMetaModel instanceof GenuineVariableMetaModel ? "genuine" - : "shadow", - variableMetaModel.name(), - entityDiff.entity(), entityDiff.entityMetaModel().type(), oldValue, newValue); - } - - @Override - public boolean equals(@Nullable Object o) { - if (!(o instanceof DefaultPlanningVariableDiff that)) { - return false; - } - return Objects.equals(variableMetaModel, that.variableMetaModel) - && Objects.equals(entityDiff, that.entityDiff); - } - - @Override - public int hashCode() { - return Objects.hash(variableMetaModel, entityDiff); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java index daef562296c..6cb170ec0f1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java @@ -26,8 +26,6 @@ import java.util.SequencedMap; import java.util.SequencedSet; import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; @@ -57,8 +55,6 @@ import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.score.descriptor.ScoreDescriptor; -import ai.timefold.solver.core.impl.domain.solution.ConstraintWeightSupplier; -import ai.timefold.solver.core.impl.domain.solution.OverridesBasedConstraintWeightSupplier; import ai.timefold.solver.core.impl.domain.solution.cloner.FieldAccessingSolutionCloner; import ai.timefold.solver.core.impl.domain.solution.cloner.gizmo.GizmoSolutionCloner; import ai.timefold.solver.core.impl.domain.solution.cloner.gizmo.GizmoSolutionClonerFactory; @@ -74,7 +70,6 @@ import ai.timefold.solver.core.impl.util.MutableLong; import ai.timefold.solver.core.impl.util.MutablePair; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; import org.jspecify.annotations.NonNull; import org.slf4j.Logger; @@ -146,11 +141,6 @@ public static SolutionDescriptor buildSolutionDescriptor( entityDescriptor.processAnnotations(descriptorPolicy); } solutionDescriptor.afterAnnotationsProcessed(descriptorPolicy); - if (solutionDescriptor.constraintWeightSupplier != null) { - // The scoreDescriptor is definitely initialized at this point. - solutionDescriptor.constraintWeightSupplier.initialize(solutionDescriptor, - descriptorPolicy.getMemberAccessorFactory(), descriptorPolicy.getDomainAccessType()); - } return solutionDescriptor; } @@ -321,7 +311,7 @@ private void processConstraintWeights(DescriptorPolicy descriptorPolicy) { "The solutionClass (%s) has a field of type (%s) which was already found on its parent class." .formatted(lineageClass, ConstraintWeightOverrides.class)); } - constraintWeightSupplier = OverridesBasedConstraintWeightSupplier.create(this, descriptorPolicy, + constraintWeightSupplier = ConstraintWeightSupplier.create(this, descriptorPolicy, constraintWeightFieldList.getFirst()); } default -> @@ -1108,70 +1098,6 @@ public > void setScore(Solution_ solution, Score_ s this. getScoreDescriptor().setScore(solution, score); } - public PlanningSolutionDiff diff(Solution_ oldSolution, Solution_ newSolution) { - // Genuine entities first, then sort by class name. - var oldEntities = sortEntitiesForDiff(oldSolution); - var newEntities = sortEntitiesForDiff(newSolution); - - var removedOldEntities = new LinkedHashSet<>(oldEntities.size()); - var oldToNewEntities = new LinkedHashMap<>(newEntities.size()); - for (var entry : oldEntities.entrySet()) { - var entityClassName = entry.getKey(); - for (var oldEntity : entry.getValue()) { - var newEntity = newEntities.getOrDefault(entityClassName, Collections.emptySet()).stream() - .filter(e -> Objects.equals(e, oldEntity)) - .findFirst() - .orElse(null); - if (newEntity == null) { - removedOldEntities.add(oldEntity); - } else { - oldToNewEntities.put(oldEntity, newEntity); - } - } - } - - var addedNewEntities = newEntities.values().stream().flatMap(Collection::stream) - .filter(newEntity -> !oldToNewEntities.containsValue(newEntity)) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - // Genuine variables first, then sort by ordinal. - var variableDescriptorComparator = Comparator - ., String> comparing( - variableDescriptor -> variableDescriptor instanceof GenuineVariableDescriptor ? "0" : "1") - .thenComparingInt(VariableDescriptor::getOrdinal); - var solutionDiff = new DefaultPlanningSolutionDiff<>(getMetaModel(), oldSolution, newSolution, removedOldEntities, - addedNewEntities); - for (var entry : oldToNewEntities.entrySet()) { - var oldEntity = entry.getKey(); - var newEntity = entry.getValue(); - var entityDescriptor = findEntityDescriptorOrFail(oldEntity.getClass()); - var entityDiff = new DefaultPlanningEntityDiff<>(solutionDiff, entry.getKey()); - entityDescriptor.getVariableDescriptorMap().values().stream().sorted(variableDescriptorComparator) - .flatMap(variableDescriptor -> { - var oldValue = variableDescriptor.getValue(oldEntity); - var newValue = variableDescriptor.getValue(newEntity); - if (Objects.equals(oldValue, newValue)) { - return Stream.empty(); - } - var variableMetaModel = entityDiff.entityMetaModel().variable(variableDescriptor.getVariableName()); - var variableDiff = new DefaultPlanningVariableDiff<>(entityDiff, variableMetaModel, oldValue, newValue); - return Stream.of(variableDiff); - }).forEach(entityDiff::addVariableDiff); - if (!entityDiff.variableDiffs().isEmpty()) { - solutionDiff.addEntityDiff(entityDiff); - } - } - return solutionDiff; - } - - private SortedMap> sortEntitiesForDiff(Solution_ solution) { - return getEntityDescriptors().stream().map(descriptor -> descriptor.extractEntities(solution)) - .flatMap(Collection::stream) - // TreeMap and LinkedHashSet for fully reproducible ordering of entities and variables. - .collect(Collectors.groupingBy(s -> s.getClass().getCanonicalName(), TreeMap::new, - Collectors.toCollection(LinkedHashSet::new))); - } - @Override public String toString() { return "%s(%s)".formatted(getClass().getSimpleName(), solutionClass.getName()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java index dd4980637a4..cb6ff541105 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java @@ -23,8 +23,7 @@ import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable; import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultShadowVariableMetaModel; @@ -40,6 +39,7 @@ import ai.timefold.solver.core.impl.domain.variable.listener.support.VariableListenerSupport; import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.InnerScore; @@ -336,12 +336,7 @@ public InnerScore calculateScore() { } @Override - public Map> getConstraintMatchTotalMap() { - throw new UnsupportedOperationException(); - } - - @Override - public Map> getIndictmentMap() { + public Map> getConstraintMatchTotalMap() { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 66132466227..4eb16153e11 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -7,7 +7,6 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.solver.event.EventProducerId; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; @@ -17,6 +16,7 @@ import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.phase.AbstractPhase; import ai.timefold.solver.core.impl.phase.PhaseType; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/MoveTesterScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/MoveTesterScoreDirector.java index d13a8d9461f..675f60b01c9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/MoveTesterScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/MoveTesterScoreDirector.java @@ -4,8 +4,8 @@ import java.util.Map; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.director.InnerScore; @@ -31,12 +31,7 @@ public InnerScore calculateScore() { } @Override - public Map> getConstraintMatchTotalMap() { - return Collections.emptyMap(); - } - - @Override - public Map> getIndictmentMap() { + public Map> getConstraintMatchTotalMap() { return Collections.emptyMap(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java b/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java deleted file mode 100644 index 3b5a09e2f63..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java +++ /dev/null @@ -1,194 +0,0 @@ -package ai.timefold.solver.core.impl.score; - -import static java.util.Comparator.comparing; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.impl.score.director.InnerScore; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; - -import org.jspecify.annotations.NonNull; - -public final class DefaultScoreExplanation> - implements ScoreExplanation { - - private static final int DEFAULT_SCORE_EXPLANATION_INDICTMENT_LIMIT = 5; - private static final int DEFAULT_SCORE_EXPLANATION_CONSTRAINT_MATCH_LIMIT = 2; - - private final Solution_ solution; - private final InnerScore innerScore; - private final Map> constraintMatchTotalMap; - private final List constraintJustificationList; - private final Map> indictmentMap; - private final AtomicReference summary = new AtomicReference<>(); // Will be calculated lazily. - - public static > String explainScore(InnerScore workingScore, - Collection> constraintMatchTotalCollection, - Collection> indictmentCollection) { - return explainScore(workingScore, constraintMatchTotalCollection, indictmentCollection, - DEFAULT_SCORE_EXPLANATION_INDICTMENT_LIMIT, DEFAULT_SCORE_EXPLANATION_CONSTRAINT_MATCH_LIMIT); - } - - public static > String explainScore(InnerScore workingScore, - Collection> constraintMatchTotalCollection, - Collection> indictmentCollection, int indictmentLimit, int constraintMatchLimit) { - var scoreExplanation = new StringBuilder((constraintMatchTotalCollection.size() + 4 + 2 * indictmentLimit) * 80); - scoreExplanation.append(""" - Explanation of score (%s)%s: - Constraint matches: - """.formatted(workingScore, workingScore.isFullyAssigned() ? "" : " for an uninitialized solution")); - - Comparator> constraintMatchTotalComparator = comparing(ConstraintMatchTotal::getScore); - Comparator> constraintMatchComparator = comparing(ConstraintMatch::getScore); - constraintMatchTotalCollection.stream() - .sorted(constraintMatchTotalComparator) - .forEach(constraintMatchTotal -> { - var constraintMatchSet = constraintMatchTotal.getConstraintMatchSet(); - scoreExplanation.append(""" - %s: constraint (%s) has %s matches: - """.formatted(constraintMatchTotal.getScore().toShortString(), - constraintMatchTotal.getConstraintRef().constraintName(), constraintMatchSet.size())); - constraintMatchSet.stream() - .sorted(constraintMatchComparator) - .limit(constraintMatchLimit) - .forEach(constraintMatch -> { - if (constraintMatch.getJustification() == null) { - scoreExplanation.append(""" - %s: unjustified - """.formatted(constraintMatch.getScore().toShortString())); - } else { - scoreExplanation.append(""" - %s: justified with (%s) - """.formatted(constraintMatch.getScore().toShortString(), - constraintMatch.getJustification())); - } - }); - if (constraintMatchSet.size() > constraintMatchLimit) { - scoreExplanation.append(""" - ... - """); - } - }); - - var indictmentCount = indictmentCollection.size(); - if (indictmentLimit < indictmentCount) { - scoreExplanation.append(""" - Indictments (top %s of %s): - """.formatted(indictmentLimit, indictmentCount)); - } else { - scoreExplanation.append(""" - Indictments: - """); - } - - Comparator> indictmentComparator = comparing(Indictment::getScore); - Comparator> constraintMatchScoreComparator = comparing(ConstraintMatch::getScore); - indictmentCollection.stream() - .sorted(indictmentComparator) - .limit(indictmentLimit) - .forEach(indictment -> { - var constraintMatchSet = indictment.getConstraintMatchSet(); - scoreExplanation.append(""" - %s: indicted with (%s) has %s matches: - """.formatted(indictment.getScore().toShortString(), indictment.getIndictedObject(), - constraintMatchSet.size())); - constraintMatchSet.stream() - .sorted(constraintMatchScoreComparator) - .limit(constraintMatchLimit) - .forEach(constraintMatch -> scoreExplanation.append(""" - %s: constraint (%s) - """.formatted(constraintMatch.getScore().toShortString(), - constraintMatch.getConstraintRef().constraintName()))); - if (constraintMatchSet.size() > constraintMatchLimit) { - scoreExplanation.append(""" - ... - """); - } - }); - if (indictmentCount > indictmentLimit) { - scoreExplanation.append(""" - ... - """); - } - return scoreExplanation.toString(); - } - - public DefaultScoreExplanation(InnerScoreDirector scoreDirector) { - this(scoreDirector.getWorkingSolution(), scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap(), - scoreDirector.getIndictmentMap()); - } - - public DefaultScoreExplanation(Solution_ solution, InnerScore innerScore, - Map> constraintMatchTotalMap, - Map> indictmentMap) { - this.solution = solution; - this.innerScore = innerScore; - this.constraintMatchTotalMap = constraintMatchTotalMap; - List workingConstraintJustificationList = new ArrayList<>(); - for (ConstraintMatchTotal constraintMatchTotal : constraintMatchTotalMap.values()) { - for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { - ConstraintJustification justification = constraintMatch.getJustification(); - if (justification != null) { - workingConstraintJustificationList.add(justification); - } - } - } - this.constraintJustificationList = - workingConstraintJustificationList.isEmpty() ? Collections.emptyList() : workingConstraintJustificationList; - this.indictmentMap = indictmentMap; - } - - @Override - public @NonNull Solution_ getSolution() { - return solution; - } - - @Override - public @NonNull Score_ getScore() { - return innerScore.raw(); - } - - @Override - public boolean isInitialized() { - return innerScore.isFullyAssigned(); - } - - @Override - public @NonNull Map> getConstraintMatchTotalMap() { - return constraintMatchTotalMap; - } - - @Override - public @NonNull List getJustificationList() { - return constraintJustificationList; - } - - @Override - public @NonNull Map> getIndictmentMap() { - return indictmentMap; - } - - @Override - public @NonNull String getSummary() { - return summary.updateAndGet(currentSummary -> Objects.requireNonNullElseGet(currentSummary, - () -> explainScore(innerScore, constraintMatchTotalMap.values(), indictmentMap.values()))); - } - - @Override - public String toString() { - return getSummary(); // So that this class can be used in strings directly. - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/ScoreUtil.java b/core/src/main/java/ai/timefold/solver/core/impl/score/ScoreUtil.java index 2a841ecfa8d..7e82b0d5eb2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/ScoreUtil.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/ScoreUtil.java @@ -39,20 +39,6 @@ the suffixedScoreToken (%s) does not end with levelSuffix (%s).""" return scoreTokens; } - public static int parseLevelAsInt(Class> scoreClass, String scoreString, String levelString) { - if (levelString.equals("*")) { - return Integer.MIN_VALUE; - } - try { - return Integer.parseInt(levelString); - } catch (NumberFormatException e) { - throw new IllegalArgumentException( - "The scoreString (%s) for the scoreClass (%s) has a levelString (%s) which is not a valid integer." - .formatted(scoreString, scoreClass.getSimpleName(), levelString), - e); - } - } - public static long parseLevelAsLong(Class> scoreClass, String scoreString, String levelString) { if (levelString.equals("*")) { return Long.MIN_VALUE; diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java similarity index 68% rename from core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java rename to core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index 40751b1e7ea..083431b4e98 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -1,12 +1,10 @@ -package ai.timefold.solver.core.api.score.constraint; +package ai.timefold.solver.core.impl.score.constraint; import static java.util.Objects.requireNonNull; -import java.util.Collection; -import java.util.List; - import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.api.solver.SolutionManager; @@ -14,8 +12,7 @@ import org.jspecify.annotations.Nullable; /** - * Retrievable from {@link ConstraintMatchTotal#getConstraintMatchSet()} - * and {@link Indictment#getConstraintMatchSet()}. + * Retrievable from {@link ConstraintMatchTotal#getConstraintMatchSet()}. * *

* This class implements {@link Comparable} for consistent ordering of constraint matches in visualizations. @@ -31,21 +28,16 @@ public final class ConstraintMatch> implements Comp private final ConstraintRef constraintRef; private final @Nullable ConstraintJustification justification; - private final List<@Nullable Object> indictedObjectList; private final Score_ score; /** * @param constraintRef unique identifier of the constraint * @param justification only null if justifications are disabled - * @param indictedObjectList never null, empty if justifications are disabled * @param score penalty or reward associated with the constraint match */ - public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, - Collection indictedObjectList, Score_ score) { + public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, Score_ score) { this.constraintRef = requireNonNull(constraintRef); this.justification = justification; - this.indictedObjectList = - requireNonNull(indictedObjectList) instanceof List list ? list : List.copyOf(indictedObjectList); this.score = requireNonNull(score); } @@ -70,24 +62,6 @@ public ConstraintRef getConstraintRef() { return (Justification_) justification; } - /** - * Returns a set of objects indicted for causing this constraint match. - *

- * This method has a different meaning based on which score director the constraint comes from. - *

    - *
  • For constraint streams, it returns the facts from the matching tuple - * (eg. [A, B] for a bi stream), unless a custom indictment mapping was provided, - * in which case it returns the return value of that function.
  • - *
  • For incremental score calculation, it returns what the calculator is implemented to return.
  • - *
  • It may return an empty list, if justification support was disabled altogether.
  • - *
- * - * @return may be empty or contain null - */ - public List<@Nullable Object> getIndictedObjectList() { - return indictedObjectList; - } - public Score_ getScore() { return score; } @@ -96,10 +70,6 @@ public Score_ getScore() { // Worker methods // ************************************************************************ - public String getIdentificationString() { - return getConstraintRef().constraintName() + "/" + justification; - } - @Override public int compareTo(ConstraintMatch other) { if (!constraintRef.equals(other.constraintRef)) { @@ -119,7 +89,7 @@ public int compareTo(ConstraintMatch other) { @Override public String toString() { - return getIdentificationString() + "=" + score; + return "%s/%s=%s".formatted(getConstraintRef().constraintName(), justification, score); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java index bac2a9ed77f..f239ad113da 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java @@ -7,7 +7,7 @@ /** * Determines whether constraint match is enabled and whether constraint match justification is enabled. * - * @see ai.timefold.solver.core.api.score.constraint.ConstraintMatch + * @see ConstraintMatch * @see ai.timefold.solver.core.api.score.stream.ConstraintJustification */ @NullMarked diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java similarity index 61% rename from core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java rename to core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java index 83d16c81915..6902f3014eb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java @@ -2,28 +2,25 @@ import static java.util.Objects.requireNonNull; -import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.api.solver.SolutionManager; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; /** * If possible, prefer using {@link SolutionManager#analyze(Object)} instead. * * @param */ -public final class DefaultConstraintMatchTotal> implements ConstraintMatchTotal, - Comparable> { +@NullMarked +public final class ConstraintMatchTotal> implements Comparable> { private final ConstraintRef constraintRef; private final Score_ constraintWeight; @@ -31,29 +28,29 @@ public final class DefaultConstraintMatchTotal> imp private final Set> constraintMatchSet = new LinkedHashSet<>(); private Score_ score; - public DefaultConstraintMatchTotal(ConstraintRef constraintRef, Score_ constraintWeight) { + public ConstraintMatchTotal(ConstraintRef constraintRef, Score_ constraintWeight) { this.constraintRef = requireNonNull(constraintRef); this.constraintWeight = requireNonNull(constraintWeight); this.score = constraintWeight.zero(); } - @Override - public @NonNull ConstraintRef getConstraintRef() { + public ConstraintRef getConstraintRef() { return constraintRef; } - @Override - public @NonNull Score_ getConstraintWeight() { + public Score_ getConstraintWeight() { return constraintWeight; } - @Override - public @NonNull Set> getConstraintMatchSet() { + public int getConstraintMatchCount() { + return constraintMatchSet.size(); + } + + public Set> getConstraintMatchSet() { return constraintMatchSet; } - @Override - public @NonNull Score_ getScore() { + public Score_ getScore() { return score; } @@ -65,45 +62,41 @@ public DefaultConstraintMatchTotal(ConstraintRef constraintRef, Score_ constrain * Creates a {@link ConstraintMatch} and adds it to the collection returned by {@link #getConstraintMatchSet()}. * It will use {@link DefaultConstraintJustification}, * whose {@link DefaultConstraintJustification#getFacts()} method will return the given list of justifications. - * Additionally, the constraint match will indict the objects in the given list of justifications. * * @param justifications never null, never empty * @param score never null * @return never null */ public ConstraintMatch addConstraintMatch(List justifications, Score_ score) { - return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), justifications, score); + return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), score); } /** * Creates a {@link ConstraintMatch} and adds it to the collection returned by {@link #getConstraintMatchSet()}. * It will be justified with the provided {@link ConstraintJustification}. - * Additionally, the constraint match will indict the objects in the given list of indicted objects. * - * @param indictedObjects never null, may be empty * @param score never null * @return never null */ - public ConstraintMatch addConstraintMatch(ConstraintJustification justification, Collection indictedObjects, - Score_ score) { - ConstraintMatch constraintMatch = new ConstraintMatch<>(constraintRef, justification, indictedObjects, score); + public ConstraintMatch addConstraintMatch(ConstraintJustification justification, Score_ score) { + var constraintMatch = new ConstraintMatch(constraintRef, justification, score); addConstraintMatch(constraintMatch); return constraintMatch; } public void addConstraintMatch(ConstraintMatch constraintMatch) { - Score_ constraintMatchScore = constraintMatch.getScore(); - this.score = this.score == null ? constraintMatchScore : this.score.add(constraintMatchScore); + var constraintMatchScore = constraintMatch.getScore(); + this.score = this.score.add(constraintMatchScore); constraintMatchSet.add(constraintMatch); } public void removeConstraintMatch(ConstraintMatch constraintMatch) { score = score.subtract(constraintMatch.getScore()); - boolean removed = constraintMatchSet.remove(constraintMatch); + var removed = constraintMatchSet.remove(constraintMatch); if (!removed) { - throw new IllegalStateException("The constraintMatchTotal (" + this - + ") could not remove constraintMatch (" + constraintMatch - + ") from its constraintMatchSet (" + constraintMatchSet + ")."); + throw new IllegalStateException( + "The constraintMatchTotal (%s) could not remove constraintMatch (%s) from its constraintMatchSet (%s)." + .formatted(this, constraintMatch, constraintMatchSet)); } } @@ -112,13 +105,13 @@ public void removeConstraintMatch(ConstraintMatch constraintMatch) { // ************************************************************************ @Override - public int compareTo(DefaultConstraintMatchTotal other) { + public int compareTo(ConstraintMatchTotal other) { return constraintRef.compareTo(other.constraintRef); } @Override public boolean equals(Object o) { - if (o instanceof DefaultConstraintMatchTotal other) { + if (o instanceof ConstraintMatchTotal other) { return constraintRef.equals(other.constraintRef); } return false; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java deleted file mode 100644 index 11257fab42e..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java +++ /dev/null @@ -1,125 +0,0 @@ -package ai.timefold.solver.core.impl.score.constraint; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.impl.util.CollectionUtils; - -import org.jspecify.annotations.NonNull; - -public final class DefaultIndictment> implements Indictment { - - private final Object indictedObject; - private final Set> constraintMatchSet = new LinkedHashSet<>(); - private List constraintJustificationList; - private Score_ score; - - public DefaultIndictment(Object indictedObject, Score_ zeroScore) { - this.indictedObject = indictedObject; - this.score = zeroScore; - } - - @Override - public @NonNull IndictedObject_ getIndictedObject() { - return (IndictedObject_) indictedObject; - } - - @Override - public @NonNull Set> getConstraintMatchSet() { - return constraintMatchSet; - } - - @Override - public @NonNull List getJustificationList() { - if (constraintJustificationList == null) { - constraintJustificationList = buildConstraintJustificationList(); - } - return constraintJustificationList; - } - - private List buildConstraintJustificationList() { - var constraintMatchSetSize = constraintMatchSet.size(); - switch (constraintMatchSetSize) { - case 0 -> { - return Collections.emptyList(); - } - case 1 -> { - return Collections.singletonList(constraintMatchSet.iterator().next().getJustification()); - } - default -> { - Set justificationSet = LinkedHashSet.newLinkedHashSet(constraintMatchSetSize); - for (ConstraintMatch constraintMatch : constraintMatchSet) { - justificationSet.add(constraintMatch.getJustification()); - } - return CollectionUtils.toDistinctList(justificationSet); - } - } - } - - @Override - public @NonNull Score_ getScore() { - return score; - } - - // ************************************************************************ - // Worker methods - // ************************************************************************ - - public void addConstraintMatch(ConstraintMatch constraintMatch) { - boolean added = addConstraintMatchWithoutFail(constraintMatch); - if (!added) { - throw new IllegalStateException("The indictment (" + this - + ") could not add constraintMatch (" + constraintMatch - + ") to its constraintMatchSet (" + constraintMatchSet + ")."); - } - } - - public boolean addConstraintMatchWithoutFail(ConstraintMatch constraintMatch) { - boolean added = constraintMatchSet.add(constraintMatch); - if (added) { - score = score.add(constraintMatch.getScore()); - constraintJustificationList = null; // Rebuild later. - } - return added; - } - - public void removeConstraintMatch(ConstraintMatch constraintMatch) { - score = score.subtract(constraintMatch.getScore()); - boolean removed = constraintMatchSet.remove(constraintMatch); - if (!removed) { - throw new IllegalStateException("The indictment (" + this - + ") could not remove constraintMatch (" + constraintMatch - + ") from its constraintMatchSet (" + constraintMatchSet + ")."); - } - constraintJustificationList = null; // Rebuild later. - } - - // ************************************************************************ - // Infrastructure methods - // ************************************************************************ - - @Override - public boolean equals(Object o) { - if (o instanceof DefaultIndictment other) { - return indictedObject.equals(other.indictedObject); - } - return false; - } - - @Override - public int hashCode() { - return indictedObject.hashCode(); - } - - @Override - public String toString() { - return indictedObject + "=" + score; - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 0cc3e8784d3..710456be2b9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -2,11 +2,9 @@ import static java.util.Objects.requireNonNull; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -16,9 +14,6 @@ import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; import ai.timefold.solver.core.api.domain.variable.ShadowVariable; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; -import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.api.solver.change.ProblemChange; import ai.timefold.solver.core.api.solver.change.ProblemChangeDirector; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -67,7 +62,6 @@ public abstract class AbstractScoreDirector, Factory_ extends AbstractScoreDirectorFactory> implements InnerScoreDirector { - private static final int CONSTRAINT_MATCH_DISPLAY_LIMIT = 8; protected final Logger logger = LoggerFactory.getLogger(getClass()); protected final Factory_ scoreDirectorFactory; @@ -281,8 +275,7 @@ protected void setWorkingSolutionWithoutUpdatingShadows(Solution_ workingSolutio var entityClassToEntitySetMap = new HashMap, Set>(); Consumer entityValidator = entity -> { var entityDescriptor = scoreDirectorFactory.validateEntity(this, entity); - var newEntity = entityClassToEntitySetMap.computeIfAbsent(entityDescriptor.getEntityClass(), - k -> new HashSet<>()) + var newEntity = entityClassToEntitySetMap.computeIfAbsent(entityDescriptor.getEntityClass(), k -> new HashSet<>()) .add(entity); if (!newEntity) { throw new IllegalStateException(""" @@ -366,19 +359,18 @@ public InnerScore executeTemporaryMove(Move move, @Nullable C if (solutionTracker != null) { solutionTracker.setBeforeMoveSolution(workingSolution); } - var result = moveDirector.executeTemporary(move, - (score, undoMove) -> { - if (solutionTracker != null) { - solutionTracker.setAfterMoveSolution(workingSolution); - } - if (assertMoveScoreFromScratch) { - assertWorkingScoreFromScratch(score, move); - } - if (consumer != null) { - consumer.accept(moveDirector); - } - return score; - }); + var result = moveDirector.executeTemporary(move, (score, undoMove) -> { + if (solutionTracker != null) { + solutionTracker.setAfterMoveSolution(workingSolution); + } + if (assertMoveScoreFromScratch) { + assertWorkingScoreFromScratch(score, move); + } + if (consumer != null) { + consumer.accept(moveDirector); + } + return score; + }); return Objects.requireNonNull(result); } @@ -394,7 +386,7 @@ private void setWorkingEntityListDirty(@Nullable Solution_ solution) { @Override public Solution_ cloneSolution(Solution_ originalSolution) { - SolutionDescriptor solutionDescriptor = getSolutionDescriptor(); + var solutionDescriptor = getSolutionDescriptor(); var originalScore = solutionDescriptor.getScore(originalSolution); var cloneSolution = solutionDescriptor.getSolutionCloner().cloneSolution(originalSolution); var cloneScore = solutionDescriptor.getScore(cloneSolution); @@ -676,7 +668,7 @@ public void afterProblemFactRemoved(Object problemFact) { @Override public void assertExpectedWorkingScore(InnerScore expectedWorkingScore, Object completedAction) { - InnerScore workingScore = calculateScore(); + var workingScore = calculateScore(); if (!expectedWorkingScore.equals(workingScore)) { throw new ScoreCorruptionException(""" Score corruption (%s): the expectedWorkingScore (%s) is not the workingScore (%s) \ @@ -710,24 +702,6 @@ public void assertShadowVariablesAreNotStale(InnerScore expectedWorkingS } } - /** - * @param predicted true if the score was predicted and might have been calculated on another thread - * @return never null - */ - protected String buildShadowVariableAnalysis(boolean predicted) { - var violationMessage = variableListenerSupport.createShadowVariablesViolationMessage(); - var workingLabel = predicted ? "working" : "corrupted"; - if (violationMessage == null) { - return """ - Shadow variable corruption in the %s scoreDirector: - None""".formatted(workingLabel); - } - return """ - Shadow variable corruption in the %s scoreDirector: - %s - Maybe there is a bug in the updater of those shadow variable(s).""".formatted(workingLabel, violationMessage); - } - @Override public void assertWorkingScoreFromScratch(InnerScore workingScore, Object completedAction) { assertScoreFromScratch(workingScore, completedAction, false); @@ -749,8 +723,9 @@ private void assertScoreFromScratch(InnerScore innerScore, Object comple uncorruptedScoreDirector.setWorkingSolution(workingSolution); var uncorruptedInnerScore = uncorruptedScoreDirector.calculateScore(); if (!innerScore.equals(uncorruptedInnerScore)) { - String scoreCorruptionAnalysis = buildScoreCorruptionAnalysis(uncorruptedScoreDirector, predicted); - String shadowVariableAnalysis = buildShadowVariableAnalysis(predicted); + var corruptionAnalyzer = new CorruptionAnalyzer<>(this); + var scoreCorruptionAnalysis = corruptionAnalyzer.analyzeScore(uncorruptedScoreDirector, predicted); + var shadowVariableAnalysis = corruptionAnalyzer.analyzeShadowVariables(predicted); throw new ScoreCorruptionException(""" Score corruption (%s): the %s (%s) is not the uncorruptedScore (%s) after completedAction (%s): %s @@ -875,8 +850,8 @@ private static void assertValueRangeForBasicVariables(InnerScoreDire if (value == null) { continue; } - var valueRange = scoreDirector.getValueRangeManager() - .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); + var valueRange = + scoreDirector.getValueRangeManager().getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); if (!valueRange.contains(value)) { throw new IllegalStateException( "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." @@ -890,8 +865,8 @@ private static void assertValueRangeForListVariable(InnerScoreDirect if (valueList.isEmpty()) { return; } - var valueRange = scoreDirector.getValueRangeManager() - .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); + var valueRange = + scoreDirector.getValueRangeManager().getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); for (var value : valueList) { if (value == null) { continue; @@ -904,135 +879,8 @@ private static void assertValueRangeForListVariable(InnerScoreDirect } } - /** - * @param uncorruptedScoreDirector never null - * @param predicted true if the score was predicted and might have been calculated on another thread - * @return never null - */ - protected String buildScoreCorruptionAnalysis(InnerScoreDirector uncorruptedScoreDirector, - boolean predicted) { - if (!getConstraintMatchPolicy().isEnabled() || !uncorruptedScoreDirector.getConstraintMatchPolicy().isEnabled()) { - return """ - Score corruption analysis could not be generated because either corrupted constraintMatchPolicy (%s) \ - or uncorrupted constraintMatchPolicy (%s) is %s. - Check your score constraints manually.""".formatted(constraintMatchPolicy, - uncorruptedScoreDirector.getConstraintMatchPolicy(), ConstraintMatchPolicy.DISABLED); - } - - var corruptedAnalysis = buildScoreAnalysis(ScoreAnalysisFetchPolicy.FETCH_ALL); - var uncorruptedAnalysis = uncorruptedScoreDirector.buildScoreAnalysis(ScoreAnalysisFetchPolicy.FETCH_ALL); - - var excessSet = new LinkedHashSet>(); - var missingSet = new LinkedHashSet>(); - - uncorruptedAnalysis.constraintMap().forEach((constraintRef, uncorruptedConstraintAnalysis) -> { - var uncorruptedConstraintMatches = emptyMatchAnalysisIfNull(uncorruptedConstraintAnalysis); - var corruptedConstraintMatches = emptyMatchAnalysisIfNull(corruptedAnalysis.constraintMap().get(constraintRef)); - if (corruptedConstraintMatches.isEmpty()) { - missingSet.addAll(uncorruptedConstraintMatches); - } else { - updateExcessAndMissingConstraintMatches(uncorruptedConstraintMatches, corruptedConstraintMatches, excessSet, - missingSet); - } - }); - - corruptedAnalysis.constraintMap().forEach((constraintRef, corruptedConstraintAnalysis) -> { - var corruptedConstraintMatches = emptyMatchAnalysisIfNull(corruptedConstraintAnalysis); - var uncorruptedConstraintMatches = emptyMatchAnalysisIfNull(uncorruptedAnalysis.constraintMap().get(constraintRef)); - if (uncorruptedConstraintMatches.isEmpty()) { - excessSet.addAll(corruptedConstraintMatches); - } else { - updateExcessAndMissingConstraintMatches(uncorruptedConstraintMatches, corruptedConstraintMatches, excessSet, - missingSet); - } - }); - - var analysis = new StringBuilder(); - analysis.append("Score corruption analysis:\n"); - // If predicted, the score calculation might have happened on another thread, so a different ScoreDirector - // so there is no guarantee that the working ScoreDirector is the corrupted ScoreDirector - var workingLabel = predicted ? "working" : "corrupted"; - appendAnalysis(analysis, workingLabel, "should not be there", excessSet); - appendAnalysis(analysis, workingLabel, "are missing", missingSet); - if (!missingSet.isEmpty() || !excessSet.isEmpty()) { - analysis.append(""" - Maybe there is a bug in the score constraints of those ConstraintMatch(s). - Maybe a score constraint doesn't select all the entities it depends on, - but discovers some transitively through a reference from the selected entity. - This corrupts incremental score calculation, - because the constraint is not re-evaluated if the transitively discovered entity changes. - """.stripTrailing()); - } else { - if (predicted) { - analysis.append(""" - If multi-threaded solving is active: - - the working scoreDirector is probably not the corrupted scoreDirector. - - maybe the rebase() method of the move is bugged. - - maybe a VariableListener affected the moveThread's workingSolution after doing and undoing a move, - but this didn't happen here on the solverThread, so we can't detect it. - """.stripTrailing()); - } else { - analysis.append(" Impossible state. Maybe this is a bug in the scoreDirector (%s).".formatted(getClass())); - } - } - return analysis.toString(); - } - - private static > List> - emptyMatchAnalysisIfNull(@Nullable ConstraintAnalysis constraintAnalysis) { - if (constraintAnalysis == null) { - return Collections.emptyList(); - } - var matches = constraintAnalysis.matches(); - if (matches == null) { - return Collections.emptyList(); - } - return matches; - } - - private void appendAnalysis(StringBuilder analysis, String workingLabel, String suffix, - Set> matches) { - if (matches.isEmpty()) { - analysis.append(""" - The %s scoreDirector has no ConstraintMatch(es) which %s. - """.formatted(workingLabel, suffix)); - } else { - analysis.append(""" - The %s scoreDirector has %s ConstraintMatch(es) which %s: - """.formatted(workingLabel, matches.size(), suffix)); - matches.stream().sorted().limit(CONSTRAINT_MATCH_DISPLAY_LIMIT).forEach(match -> analysis.append(""" - %s/%s=%s - """.formatted(match.constraintRef().constraintName(), match.justification(), match.score()))); - if (matches.size() >= CONSTRAINT_MATCH_DISPLAY_LIMIT) { - analysis.append(""" - ... %s more - """.formatted(matches.size() - CONSTRAINT_MATCH_DISPLAY_LIMIT)); - } - } - } - - private void updateExcessAndMissingConstraintMatches(List> uncorruptedList, - List> corruptedList, Set> excessSet, - Set> missingSet) { - iterateAndAddIfFound(corruptedList, uncorruptedList, excessSet); - iterateAndAddIfFound(uncorruptedList, corruptedList, missingSet); - } - - private void iterateAndAddIfFound(List> referenceList, List> lookupList, - Set> targetSet) { - if (referenceList.isEmpty()) { - return; - } - var lookupSet = new LinkedHashSet<>(lookupList); // Guaranteed to not contain duplicates anyway. - for (var reference : referenceList) { - if (!lookupSet.contains(reference)) { - targetSet.add(reference); - } - } - } - - protected boolean isConstraintConfiguration(Object problemFactOrEntity) { - var solutionDescriptor = scoreDirectorFactory.getSolutionDescriptor(); + private boolean isConstraintConfiguration(Object problemFactOrEntity) { + var solutionDescriptor = getSolutionDescriptor(); var constraintWeightSupplier = solutionDescriptor.getConstraintWeightSupplier(); if (constraintWeightSupplier == null) { return false; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/CorruptionAnalyzer.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/CorruptionAnalyzer.java new file mode 100644 index 00000000000..d8756a305b5 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/CorruptionAnalyzer.java @@ -0,0 +1,195 @@ +package ai.timefold.solver.core.impl.score.director; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.domain.variable.listener.support.VariableListenerSupport; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +final class CorruptionAnalyzer> { + + private static final int CONSTRAINT_MATCH_DISPLAY_LIMIT = 8; + + private final InnerScoreDirector scoreDirector; + + public CorruptionAnalyzer(InnerScoreDirector scoreDirector) { + this.scoreDirector = Objects.requireNonNull(scoreDirector); + } + + /** + * @param uncorruptedScoreDirector never null + * @param predicted true if the score was predicted and might have been calculated on another thread + * @return never null + */ + public String analyzeScore(InnerScoreDirector uncorruptedScoreDirector, boolean predicted) { + if (!scoreDirector.getConstraintMatchPolicy().isEnabled() + || !uncorruptedScoreDirector.getConstraintMatchPolicy().isEnabled()) { + return """ + Score corruption analysis could not be generated because either corrupted constraintMatchPolicy (%s) \ + or uncorrupted constraintMatchPolicy (%s) is %s. + Check your score constraints manually.""".formatted(scoreDirector.getConstraintMatchPolicy(), + uncorruptedScoreDirector.getConstraintMatchPolicy(), ConstraintMatchPolicy.DISABLED); + } + + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + var corruptedMap = createConstraintMatchMap(constraintMatchTotalMap.values()); + var uncorruptedConstraintMatchTotalMap = uncorruptedScoreDirector.getConstraintMatchTotalMap(); + var uncorruptedMap = createConstraintMatchMap(uncorruptedConstraintMatchTotalMap.values()); + + var excessSet = new LinkedHashSet>(); + var missingSet = new LinkedHashSet>(); + + uncorruptedMap.forEach((key, uncorruptedMatches) -> { + var corruptedMatches = corruptedMap.getOrDefault(key, Collections.emptySet()); + if (corruptedMatches.isEmpty()) { + missingSet.addAll(uncorruptedMatches); + return; + } + updateExcessAndMissingConstraintMatches(uncorruptedMatches, corruptedMatches, excessSet, missingSet); + }); + + corruptedMap.forEach((key, corruptedMatches) -> { + var uncorruptedMatches = uncorruptedMap.getOrDefault(key, Collections.emptySet()); + if (uncorruptedMatches.isEmpty()) { + excessSet.addAll(corruptedMatches); + return; + } + updateExcessAndMissingConstraintMatches(uncorruptedMatches, corruptedMatches, excessSet, missingSet); + }); + + var analysis = new StringBuilder(); + analysis.append(""" + Score corruption analysis: + """); + // If predicted, the score calculation might have happened on another thread, so a different ScoreDirector + // so there is no guarantee that the working ScoreDirector is the corrupted ScoreDirector + var workingLabel = predicted ? "working" : "corrupted"; + appendAnalysis(analysis, workingLabel, "should not be there", excessSet); + appendAnalysis(analysis, workingLabel, "are missing", missingSet); + if (!missingSet.isEmpty() || !excessSet.isEmpty()) { + analysis.append(""" + Maybe there is a bug in the score constraints of those ConstraintMatch(s). + Maybe a score constraint doesn't select all the entities it depends on, + but discovers some transitively through a reference from the selected entity. + This corrupts incremental score calculation, + because the constraint is not re-evaluated if the transitively discovered entity changes. + """.stripTrailing()); + } else { + if (predicted) { + analysis.append(""" + If multi-threaded solving is active: + - the working scoreDirector is probably not the corrupted scoreDirector. + - maybe the rebase() method of the move is bugged. + - maybe a VariableListener affected the moveThread's workingSolution after doing and undoing a move, + but this didn't happen here on the solverThread, so we can't detect it. + """.stripTrailing()); + } else { + analysis.append(" Impossible state. Maybe this is a bug in the scoreDirector (%s).".formatted(getClass())); + } + } + return analysis.toString(); + } + + private static > Map>> + createConstraintMatchMap(Collection> constraintMatchTotals) { + var constraintMatchMap = + LinkedHashMap.>> newLinkedHashMap(constraintMatchTotals.size() * 16); + for (var constraintMatchTotal : constraintMatchTotals) { + var constraintId = constraintMatchTotal.getConstraintRef(); + for (var constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { + var keyStream = Stream.builder().add(constraintId); + var justification = constraintMatch.getJustification(); + keyStream.add(justification); + // And now we store the reference to the constraint match. + // Constraint Streams with indistinct tuples may produce two different match instances for the same key. + var key = keyStream.add(constraintMatch.getScore()).build().collect(Collectors.toList()); + var added = constraintMatchMap.computeIfAbsent(key, k -> new LinkedHashSet<>(0)).add(constraintMatch); + if (!added) { + throw new IllegalStateException( + "Score corruption because the constraintMatch (%s) was added twice for constraintMatchTotal (%s) without removal." + .formatted(constraintMatch, constraintMatchTotal)); + } + } + } + return constraintMatchMap; + } + + private static > void updateExcessAndMissingConstraintMatches( + Set> uncorruptedSet, Set> corruptedSet, + Set> excessSet, Set> missingSet) { + var uncorruptedMatchCount = (long) uncorruptedSet.size(); + var corruptedMatchCount = (long) corruptedSet.size(); + /* + * The corrupted and uncorrupted sets contain 1+ constraint matches which are the same. + * (= They have the same constraint, same justifications and the same score.) + * This is perfectly fine and happens when a constraint stream produces duplicate tuples. + * + * It is expected that the number of these matches would be the same between the two sets. + * When it is not, it is a sign of score corruption. + * In that case, for visualization purposes, we need to take the excess and/or missing constraint matches, + * and print them to the user. + * It does not matter which ones we pick, because they are all the same. + * So we just use the limit() below to pick the first ones. + */ + if (corruptedMatchCount > uncorruptedMatchCount) { + corruptedSet.stream().limit(corruptedMatchCount - uncorruptedMatchCount).forEach(excessSet::add); + } else if (corruptedMatchCount < uncorruptedMatchCount) { + uncorruptedSet.stream().limit(uncorruptedMatchCount - corruptedMatchCount).forEach(missingSet::add); + } + } + + private static > void appendAnalysis(StringBuilder analysis, String workingLabel, + String suffix, Set> matches) { + if (matches.isEmpty()) { + analysis.append(""" + The %s scoreDirector has no ConstraintMatch(es) which %s. + """.formatted(workingLabel, suffix)); + } else { + analysis.append(""" + The %s scoreDirector has %s ConstraintMatch(es) which %s: + """.formatted(workingLabel, matches.size(), suffix)); + matches.stream().sorted().limit(CONSTRAINT_MATCH_DISPLAY_LIMIT).forEach(match -> analysis.append(""" + %s + """.formatted(match))); + if (matches.size() >= CONSTRAINT_MATCH_DISPLAY_LIMIT) { + analysis.append(""" + ... %s more + """.formatted(matches.size() - CONSTRAINT_MATCH_DISPLAY_LIMIT)); + } + } + } + + /** + * @param predicted true if the score was predicted and might have been calculated on another thread + * @return never null + */ + @SuppressWarnings("unchecked") + public String analyzeShadowVariables(boolean predicted) { + var violationMessage = + ((VariableListenerSupport) scoreDirector.getSupplyManager()).createShadowVariablesViolationMessage(); + var workingLabel = predicted ? "working" : "corrupted"; + if (violationMessage == null) { + return """ + Shadow variable corruption in the %s scoreDirector: + None""".formatted(workingLabel); + } + return """ + Shadow variable corruption in the %s scoreDirector: + %s + Maybe there is a bug in the updater of those shadow variable(s).""".formatted(workingLabel, violationMessage); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 5a3bfaca11c..11cf8804fca 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -1,28 +1,13 @@ package ai.timefold.solver.core.impl.score.director; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; - -import java.util.List; import java.util.Map; -import java.util.TreeMap; import java.util.function.Consumer; -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; -import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.score.stream.Constraint; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -35,6 +20,7 @@ import ai.timefold.solver.core.impl.neighborhood.NeighborhoodsBasedMoveRepository; import ai.timefold.solver.core.impl.phase.scope.SolverLifecyclePoint; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; import ai.timefold.solver.core.preview.api.move.Move; @@ -51,36 +37,6 @@ public interface InnerScoreDirector> extends VariableDescriptorAwareScoreDirector, AutoCloseable { - static > ConstraintAnalysis getConstraintAnalysis( - ConstraintMatchTotal constraintMatchTotal, ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { - return switch (scoreAnalysisFetchPolicy) { - case FETCH_ALL -> { - // Justification can not be null here, because they are enabled by FETCH_ALL. - var deduplicatedConstraintMatchMap = constraintMatchTotal.getConstraintMatchSet().stream() - .collect(groupingBy(c -> (ConstraintJustification) c.getJustification(), toList())); - var matchAnalyses = sumMatchesWithSameJustification(constraintMatchTotal, deduplicatedConstraintMatchMap); - yield new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), - constraintMatchTotal.getConstraintWeight(), constraintMatchTotal.getScore(), matchAnalyses); - } - case FETCH_MATCH_COUNT -> - new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), - constraintMatchTotal.getScore(), null, constraintMatchTotal.getConstraintMatchCount()); - case FETCH_SHALLOW -> - new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), - constraintMatchTotal.getScore(), null); - }; - } - - private static > List> sumMatchesWithSameJustification( - ConstraintMatchTotal constraintMatchTotal, - Map>> deduplicatedConstraintMatchMap) { - return deduplicatedConstraintMatchMap.entrySet().stream().map(entry -> { - var score = entry.getValue().stream().map(ConstraintMatch::getScore).reduce(constraintMatchTotal.getScore().zero(), - Score::add); - return new MatchAnalysis<>(constraintMatchTotal.getConstraintRef(), score, entry.getKey()); - }).toList(); - } - /** * Sets the {@link PlanningSolution working solution} of the {@link ScoreDirector} * to the given {@link PlanningSolution solution}, and force updates all the shadow variables on the given solution. @@ -126,8 +82,7 @@ private static > List> sumMat InnerScore calculateScore(); /** - * @return {@link ConstraintMatchPolicy#ENABLED} if {@link #getConstraintMatchTotalMap()} and {@link #getIndictmentMap()} - * can be called. + * @return {@link ConstraintMatchPolicy#ENABLED} if {@link #getConstraintMatchTotalMap()} can be called. * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS} if only the former can be called. * {@link ConstraintMatchPolicy#DISABLED} if neither can be called. */ @@ -145,28 +100,8 @@ private static > List> sumMat * If a constraint is present in the problem but resulted in no matches, * it will still be in the map with a {@link ConstraintMatchTotal#getConstraintMatchSet()} size of 0. * @throws IllegalStateException if {@link #getConstraintMatchPolicy()} returns {@link ConstraintMatchPolicy#DISABLED}. - * @see #getIndictmentMap() */ - Map> getConstraintMatchTotalMap(); - - /** - * Explains the impact of each planning entity or problem fact on the {@link Score}. - * An {@link Indictment} is basically the inverse of a {@link ConstraintMatchTotal}: - * it is a {@link Score} total for each {@link ConstraintMatch#getJustification() constraint justification}. - *

- * The sum of {@link ConstraintMatchTotal#getScore()} differs from {@link #calculateScore()} - * because each {@link ConstraintMatch#getScore()} is counted - * for each {@link ConstraintMatch#getJustification() constraint justification}. - *

- * Call {@link #calculateScore()} before calling this method, - * unless that method has already been called since the last {@link PlanningVariable} changes. - * - * @return never null, the key is a {@link ProblemFactCollectionProperty problem fact} or a - * {@link PlanningEntity planning entity} - * @throws IllegalStateException unless {@link #getConstraintMatchPolicy()} returns {@link ConstraintMatchPolicy#ENABLED}. - * @see #getConstraintMatchTotalMap() - */ - Map> getIndictmentMap(); + Map> getConstraintMatchTotalMap(); /** * @return used to check {@link #isWorkingEntityListDirty(long)} later on @@ -391,16 +326,6 @@ default boolean isDerived() { return false; } - default ScoreAnalysis buildScoreAnalysis(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { - var state = calculateScore(); - var constraintAnalysisMap = new TreeMap>(); - for (var constraintMatchTotal : getConstraintMatchTotalMap().values()) { - var constraintAnalysis = getConstraintAnalysis(constraintMatchTotal, scoreAnalysisFetchPolicy); - constraintAnalysisMap.put(constraintMatchTotal.getConstraintRef(), constraintAnalysis); - } - return new ScoreAnalysis<>(state.raw(), constraintAnalysisMap, state.isFullyAssigned()); - } - default void beforeEntityAdded(Object entity) { beforeEntityAdded(getSolutionDescriptor().findEntityDescriptorOrFail(entity.getClass()), entity); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java index cff289ebc15..b73ed455239 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java @@ -42,8 +42,4 @@ public interface ScoreDirectorFactory> { */ void assertScoreFromScratch(Solution_ solution); - default boolean supportsConstraintMatching() { - return false; - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java index 08f3c3aa547..af46f85a62b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java @@ -6,9 +6,9 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.incremental.IncrementalScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; @@ -62,7 +62,10 @@ public ScoreDirectorFactory buildScoreDirectorFactory(Environ if (config.getEasyScoreCalculatorClass() != null) { return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); } else if (config.getIncrementalScoreCalculatorClass() != null) { - return IncrementalScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); + var timefoldSolverEnterpriseService = TimefoldSolverEnterpriseService + .loadOrFail(TimefoldSolverEnterpriseService.Feature.INCREMENTAL_SCORE_CALCULATOR); + return timefoldSolverEnterpriseService.buildIncrementalScoreDirectorFactory(config, solutionDescriptor, + environmentMode); } else if (config.getConstraintProviderClass() != null) { return BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java index 562e2c1da07..f23921cea6b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java @@ -5,12 +5,12 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; +import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.director.ScoreDirector; @@ -21,8 +21,7 @@ /** * Easy java implementation of {@link ScoreDirector}, which recalculates the {@link Score} * of the {@link PlanningSolution working solution} every time. This is non-incremental calculation, which is slow. - * This score director implementation does not support {@link ScoreExplanation#getConstraintMatchTotalMap()} and - * {@link ScoreExplanation#getIndictmentMap()}. + * This score director implementation does not support {@link ScoreAnalysis}. * * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the score type to go with the solution @@ -60,23 +59,11 @@ public void setWorkingSolutionWithoutUpdatingShadows(Solution_ workingSolution) /** * {@link ConstraintMatch}s are not supported by this {@link ScoreDirector} implementation. * - * @throws IllegalStateException always * @return throws {@link IllegalStateException} - */ - @Override - public Map> getConstraintMatchTotalMap() { - throw new IllegalStateException("%s is not supported by %s." - .formatted(ConstraintMatch.class.getSimpleName(), EasyScoreDirector.class.getSimpleName())); - } - - /** - * {@link ConstraintMatch}s are not supported by this {@link ScoreDirector} implementation. - * * @throws IllegalStateException always - * @return throws {@link IllegalStateException} */ @Override - public Map> getIndictmentMap() { + public Map> getConstraintMatchTotalMap() { throw new IllegalStateException("%s is not supported by %s." .formatted(ConstraintMatch.class.getSimpleName(), EasyScoreDirector.class.getSimpleName())); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java deleted file mode 100644 index 915554a32d6..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java +++ /dev/null @@ -1,281 +0,0 @@ -package ai.timefold.solver.core.impl.score.director.incremental; - -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; -import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; -import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; -import ai.timefold.solver.core.impl.score.director.InnerScore; -import ai.timefold.solver.core.impl.score.director.ScoreDirector; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -/** - * Incremental java implementation of {@link ScoreDirector}, which only recalculates the {@link Score} - * of the part of the {@link PlanningSolution working solution} that changed, - * instead of the going through the entire {@link PlanningSolution}. This is incremental calculation, which is fast. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @param the score type to go with the solution - * @see ScoreDirector - */ -@NullMarked -public final class IncrementalScoreDirector> - extends AbstractScoreDirector> { - - private final IncrementalScoreCalculator incrementalScoreCalculator; - - private IncrementalScoreDirector(Builder builder) { - super(builder); - this.incrementalScoreCalculator = Objects.requireNonNull(builder.incrementalScoreCalculator, - "The incrementalScoreCalculator must not be null."); - } - - public IncrementalScoreCalculator getIncrementalScoreCalculator() { - return incrementalScoreCalculator; - } - - // ************************************************************************ - // Complex methods - // ************************************************************************ - - @Override - public void setWorkingSolutionWithoutUpdatingShadows(Solution_ workingSolution) { - super.setWorkingSolutionWithoutUpdatingShadows(workingSolution, null); - if (incrementalScoreCalculator instanceof ConstraintMatchAwareIncrementalScoreCalculator) { - ((ConstraintMatchAwareIncrementalScoreCalculator) incrementalScoreCalculator) - .resetWorkingSolution(workingSolution, getConstraintMatchPolicy().isEnabled()); - } else { - incrementalScoreCalculator.resetWorkingSolution(workingSolution); - } - } - - @Override - public InnerScore calculateScore() { - variableListenerSupport.assertNotificationQueuesAreEmpty(); - var score = Objects.requireNonNull(incrementalScoreCalculator.calculateScore(), - () -> "The incrementalScoreCalculator (%s) must return a non-null score in the method calculateScore()." - .formatted(incrementalScoreCalculator)); - setCalculatedScore(score); - return new InnerScore<>(score, -getWorkingInitScore()); - } - - @Override - public Map> getConstraintMatchTotalMap() { - if (!constraintMatchPolicy.isEnabled()) { - throw new IllegalStateException("When constraint matching (" + constraintMatchPolicy - + ") is disabled in the constructor, this method should not be called."); - } - // Notice that we don't trigger the variable listeners - return ((ConstraintMatchAwareIncrementalScoreCalculator) incrementalScoreCalculator) - .getConstraintMatchTotals() - .stream() - .collect(toMap(c -> c.getConstraintRef().constraintName(), identity())); - } - - @Override - public Map> getIndictmentMap() { - if (!constraintMatchPolicy.isJustificationEnabled()) { - throw new IllegalStateException("When constraint matching with justifications (" + constraintMatchPolicy - + ") is disabled in the constructor, this method should not be called."); - } - Map> incrementalIndictmentMap = - ((ConstraintMatchAwareIncrementalScoreCalculator) incrementalScoreCalculator) - .getIndictmentMap(); - if (incrementalIndictmentMap != null) { - return incrementalIndictmentMap; - } - Map> indictmentMap = new LinkedHashMap<>(); - Score_ zeroScore = getScoreDefinition().getZeroScore(); - Map> constraintMatchTotalMap = getConstraintMatchTotalMap(); - for (ConstraintMatchTotal constraintMatchTotal : constraintMatchTotalMap.values()) { - for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { - constraintMatch.getIndictedObjectList() - .stream() - .filter(Objects::nonNull) - .distinct() // One match might have the same indictment twice. - .forEach(fact -> { - DefaultIndictment indictment = - (DefaultIndictment) indictmentMap.computeIfAbsent(fact, - k -> new DefaultIndictment<>(fact, zeroScore)); - indictment.addConstraintMatch(constraintMatch); - }); - } - } - return indictmentMap; - } - - @Override - public boolean requiresFlushing() { - return true; // Incremental may decide to keep events for delayed processing. - } - - // ************************************************************************ - // Entity/variable add/change/remove methods - // ************************************************************************ - - @Override - public void beforeEntityAdded(EntityDescriptor entityDescriptor, Object entity) { - incrementalScoreCalculator.beforeEntityAdded(entity); - super.beforeEntityAdded(entityDescriptor, entity); - } - - @Override - public void afterEntityAdded(EntityDescriptor entityDescriptor, Object entity) { - incrementalScoreCalculator.afterEntityAdded(entity); - super.afterEntityAdded(entityDescriptor, entity); - } - - @Override - public void beforeVariableChanged(VariableDescriptor variableDescriptor, Object entity) { - incrementalScoreCalculator.beforeVariableChanged(entity, variableDescriptor.getVariableName()); - super.beforeVariableChanged(variableDescriptor, entity); - } - - @Override - public void afterVariableChanged(VariableDescriptor variableDescriptor, Object entity) { - incrementalScoreCalculator.afterVariableChanged(entity, variableDescriptor.getVariableName()); - super.afterVariableChanged(variableDescriptor, entity); - } - - @Override - public void beforeListVariableElementAssigned(ListVariableDescriptor variableDescriptor, Object element) { - incrementalScoreCalculator.beforeListVariableElementAssigned(variableDescriptor.getVariableName(), element); - super.beforeListVariableElementAssigned(variableDescriptor, element); - } - - @Override - public void afterListVariableElementAssigned(ListVariableDescriptor variableDescriptor, Object element) { - incrementalScoreCalculator.afterListVariableElementAssigned(variableDescriptor.getVariableName(), element); - super.afterListVariableElementAssigned(variableDescriptor, element); - } - - @Override - public void beforeListVariableElementUnassigned(ListVariableDescriptor variableDescriptor, Object element) { - incrementalScoreCalculator.beforeListVariableElementUnassigned(variableDescriptor.getVariableName(), element); - super.beforeListVariableElementUnassigned(variableDescriptor, element); - } - - @Override - public void afterListVariableElementUnassigned(ListVariableDescriptor variableDescriptor, Object element) { - incrementalScoreCalculator.afterListVariableElementUnassigned(variableDescriptor.getVariableName(), element); - super.afterListVariableElementUnassigned(variableDescriptor, element); - } - - @Override - public void beforeListVariableChanged(ListVariableDescriptor variableDescriptor, Object entity, int fromIndex, - int toIndex) { - incrementalScoreCalculator.beforeListVariableChanged(entity, variableDescriptor.getVariableName(), fromIndex, toIndex); - super.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); - } - - @Override - public void afterListVariableChanged(ListVariableDescriptor variableDescriptor, Object entity, int fromIndex, - int toIndex) { - incrementalScoreCalculator.afterListVariableChanged(entity, variableDescriptor.getVariableName(), fromIndex, toIndex); - super.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); - } - - @Override - public void beforeEntityRemoved(EntityDescriptor entityDescriptor, Object entity) { - incrementalScoreCalculator.beforeEntityRemoved(entity); - super.beforeEntityRemoved(entityDescriptor, entity); - } - - @Override - public void afterEntityRemoved(EntityDescriptor entityDescriptor, Object entity) { - incrementalScoreCalculator.afterEntityRemoved(entity); - super.afterEntityRemoved(entityDescriptor, entity); - } - - // ************************************************************************ - // Problem fact add/change/remove methods - // ************************************************************************ - - @Override - public void beforeProblemFactAdded(Object problemFact) { - super.beforeProblemFactAdded(problemFact); - } - - @Override - public void afterProblemFactAdded(Object problemFact) { - incrementalScoreCalculator.resetWorkingSolution(workingSolution); // TODO do not nuke it - super.afterProblemFactAdded(problemFact); - } - - @Override - public void beforeProblemPropertyChanged(Object problemFactOrEntity) { - super.beforeProblemPropertyChanged(problemFactOrEntity); - } - - @Override - public void afterProblemPropertyChanged(Object problemFactOrEntity) { - incrementalScoreCalculator.resetWorkingSolution(workingSolution); // TODO do not nuke it - super.afterProblemPropertyChanged(problemFactOrEntity); - } - - @Override - public void beforeProblemFactRemoved(Object problemFact) { - super.beforeProblemFactRemoved(problemFact); - } - - @Override - public void afterProblemFactRemoved(Object problemFact) { - incrementalScoreCalculator.resetWorkingSolution(workingSolution); // TODO do not nuke it - super.afterProblemFactRemoved(problemFact); - } - - @NullMarked - public static final class Builder> - extends - AbstractScoreDirectorBuilder, Builder> { - - private @Nullable IncrementalScoreCalculator incrementalScoreCalculator; - - public Builder(IncrementalScoreDirectorFactory scoreDirectorFactory) { - super(scoreDirectorFactory); - } - - public Builder - withIncrementalScoreCalculator(IncrementalScoreCalculator incrementalScoreCalculator) { - this.incrementalScoreCalculator = incrementalScoreCalculator; - return withConstraintMatchPolicy(constraintMatchPolicy); // Ensure the policy is correct for the calculator. - } - - @Override - public Builder withConstraintMatchPolicy(ConstraintMatchPolicy constraintMatchPolicy) { - return super.withConstraintMatchPolicy(determineCorrectPolicy(constraintMatchPolicy, incrementalScoreCalculator)); - } - - @Override - public IncrementalScoreDirector build() { - return new IncrementalScoreDirector<>(this); - } - - private static ConstraintMatchPolicy determineCorrectPolicy(ConstraintMatchPolicy constraintMatchPolicy, - @Nullable IncrementalScoreCalculator incrementalScoreCalculator) { - if (incrementalScoreCalculator instanceof ConstraintMatchAwareIncrementalScoreCalculator) { - return constraintMatchPolicy; - } else { - return ConstraintMatchPolicy.DISABLED; - } - } - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java deleted file mode 100644 index 6793844bbdf..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -package ai.timefold.solver.core.impl.score.director.incremental; - -import java.util.function.Supplier; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; -import ai.timefold.solver.core.config.solver.EnvironmentMode; -import ai.timefold.solver.core.config.util.ConfigUtils; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; - -/** - * Incremental implementation of {@link ScoreDirectorFactory}. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @param the score type to go with the solution - * @see IncrementalScoreDirector - * @see ScoreDirectorFactory - */ -public final class IncrementalScoreDirectorFactory> - extends AbstractScoreDirectorFactory> { - - public static > IncrementalScoreDirectorFactory - buildScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config, - EnvironmentMode environmentMode) { - if (!IncrementalScoreCalculator.class.isAssignableFrom(config.getIncrementalScoreCalculatorClass())) { - throw new IllegalArgumentException( - "The incrementalScoreCalculatorClass (%s) does not implement %s." - .formatted(config.getIncrementalScoreCalculatorClass(), - IncrementalScoreCalculator.class.getSimpleName())); - } - return new IncrementalScoreDirectorFactory<>(solutionDescriptor, () -> { - IncrementalScoreCalculator incrementalScoreCalculator = ConfigUtils.newInstance(config, - "incrementalScoreCalculatorClass", config.getIncrementalScoreCalculatorClass()); - ConfigUtils.applyCustomProperties(incrementalScoreCalculator, "incrementalScoreCalculatorClass", - config.getIncrementalScoreCalculatorCustomProperties(), "incrementalScoreCalculatorCustomProperties"); - return incrementalScoreCalculator; - }, environmentMode); - } - - private final Supplier> incrementalScoreCalculatorSupplier; - - public IncrementalScoreDirectorFactory(SolutionDescriptor solutionDescriptor, - Supplier> incrementalScoreCalculatorSupplier, - EnvironmentMode environmentMode) { - super(solutionDescriptor, environmentMode); - this.incrementalScoreCalculatorSupplier = incrementalScoreCalculatorSupplier; - } - - @Override - public boolean supportsConstraintMatching() { - return incrementalScoreCalculatorSupplier.get() instanceof ConstraintMatchAwareIncrementalScoreCalculator; - } - - @Override - public IncrementalScoreDirector.Builder createScoreDirectorBuilder() { - return new IncrementalScoreDirector.Builder<>(this) - .withIncrementalScoreCalculator(incrementalScoreCalculatorSupplier.get()); - } - - @Override - public IncrementalScoreDirector buildScoreDirector() { - return createScoreDirectorBuilder().build(); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java index ae5aa3e2a46..8cfba1587fe 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java @@ -7,12 +7,12 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.declarative.ConsistencyTracker; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.director.ScoreDirector; @@ -97,7 +97,7 @@ public InnerScore calculateScore() { } @Override - public Map> getConstraintMatchTotalMap() { + public Map> getConstraintMatchTotalMap() { if (!constraintMatchPolicy.isEnabled()) { throw new IllegalStateException("When constraint matching is disabled, this method should not be called."); } else if (workingSolution == null) { @@ -107,18 +107,6 @@ public Map> getConstraintMatchTotalMap() { return session.getConstraintMatchTotalMap(); } - @Override - public Map> getIndictmentMap() { - if (!constraintMatchPolicy.isJustificationEnabled()) { - throw new IllegalStateException( - "When constraint matching with justifications is disabled, this method should not be called."); - } else if (workingSolution == null) { - throw new IllegalStateException( - "The method setWorkingSolution() must be called before the method getIndictmentMap()."); - } - return session.getIndictmentMap(); - } - @Override public boolean requiresFlushing() { return true; // Tuple refresh happens during score calculation. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/DefaultConstraintMetaModel.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/DefaultConstraintMetaModel.java index af246fc2bc9..d3f58cf17b7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/DefaultConstraintMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/DefaultConstraintMetaModel.java @@ -9,9 +9,9 @@ import java.util.Set; import java.util.TreeMap; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraint.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraint.java index 797e0d04ba9..7ac5cffa49f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraint.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraint.java @@ -3,7 +3,7 @@ import java.util.Set; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; @@ -16,10 +16,9 @@ public final class BavetConstraint extends public BavetConstraint(BavetConstraintFactory constraintFactory, ConstraintRef constraintRef, String description, String constraintGroup, Score constraintWeight, ScoreImpactType scoreImpactType, - Object justificationMapping, Object indictedObjectsMapping, - BavetScoringConstraintStream scoringConstraintStream) { + Object justificationMapping, BavetScoringConstraintStream scoringConstraintStream) { super(constraintFactory, constraintRef, description, constraintGroup, constraintWeight, scoreImpactType, - justificationMapping, indictedObjectsMapping); + justificationMapping); this.scoringConstraintStream = scoringConstraintStream; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java index a11c46f18fa..ce443f01200 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java @@ -3,13 +3,13 @@ import java.util.Map; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.impl.bavet.AbstractSession; import ai.timefold.solver.core.impl.bavet.NodeNetwork; import ai.timefold.solver.core.impl.bavet.common.PropagationQueue; import ai.timefold.solver.core.impl.domain.variable.declarative.ConsistencyTracker; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.score.stream.common.inliner.AbstractScoreInliner; @@ -44,12 +44,8 @@ public AbstractScoreInliner getScoreInliner() { return scoreInliner; } - public Map> getConstraintMatchTotalMap() { - return scoreInliner.getConstraintIdToConstraintMatchTotalMap(); - } - - public Map> getIndictmentMap() { - return scoreInliner.getIndictmentMap(); + public Map> getConstraintMatchTotalMap() { + return scoreInliner.getConstraintMatchTotalMap(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java index b420edf721f..8563c6986fd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java @@ -1,10 +1,8 @@ package ai.timefold.solver.core.impl.score.stream.bavet.bi; -import static ai.timefold.solver.core.impl.score.stream.common.bi.InnerBiConstraintStream.createDefaultIndictedObjectsMapping; import static ai.timefold.solver.core.impl.score.stream.common.bi.InnerBiConstraintStream.createDefaultJustificationMapping; import java.math.BigDecimal; -import java.util.Collection; import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Function; @@ -478,9 +476,9 @@ private > BiConstraintBuilderImpl new BavetScoringConstraintStream stream, ScoreImpactType impactType, Score_ constraintWeight) { return new BiConstraintBuilderImpl<>( (constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, - justificationMapping, indictedObjectsMapping) -> buildConstraint(constraintName, + justificationMapping) -> buildConstraint(constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, justificationMapping, - indictedObjectsMapping, stream), + stream), impactType, constraintWeight); } @@ -496,9 +494,4 @@ protected final TriFunction, DefaultConstraintJustification> getD return createDefaultJustificationMapping(); } - @Override - protected final BiFunction> getDefaultIndictedObjectsMapping() { - return createDefaultIndictedObjectsMapping(); - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java index b47eb217d9b..a24737b796e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java @@ -31,7 +31,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, BiTuple impactFull(WeightedScoreImpacter impacter, BiTuple> QuadConstraintBuilderImpl stream, Score_ constraintWeight, ScoreImpactType impactType) { return new QuadConstraintBuilderImpl<>( (constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, - justificationMapping, indictedObjectsMapping) -> buildConstraint(constraintName, + justificationMapping) -> buildConstraint(constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, justificationMapping, - indictedObjectsMapping, stream), + stream), impactType, constraintWeight); } @@ -441,9 +439,4 @@ protected final PentaFunction, DefaultConstraintJustificati return createDefaultJustificationMapping(); } - @Override - protected final QuadFunction> getDefaultIndictedObjectsMapping() { - return createDefaultIndictedObjectsMapping(); - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java index 7571c47555f..acbe999c17c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java @@ -35,7 +35,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, QuadTuple var d = tuple.getD(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a, b, c, d), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), constraint.getIndictedObjectsMapping(), a, b, + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, c, d)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java index 2eedac1a3bd..c58f81da4c0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java @@ -33,7 +33,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, QuadTuple var d = tuple.getD(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a, b, c, d), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), constraint.getIndictedObjectsMapping(), a, b, + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, c, d)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java index a79f7266f89..e208953fdf7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java @@ -5,11 +5,9 @@ import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.threeKeysGroupBy; import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.twoKeysGroupBy; import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.zeroKeysGroupBy; -import static ai.timefold.solver.core.impl.score.stream.common.tri.InnerTriConstraintStream.createDefaultIndictedObjectsMapping; import static ai.timefold.solver.core.impl.score.stream.common.tri.InnerTriConstraintStream.createDefaultJustificationMapping; import java.math.BigDecimal; -import java.util.Collection; import java.util.function.BiFunction; import java.util.function.Function; @@ -474,9 +472,9 @@ public > TriConstraintBuilder inne newTerminator(BavetScoringConstraintStream stream, Score_ constraintWeight, ScoreImpactType impactType) { return new TriConstraintBuilderImpl<>( (constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, - justificationMapping, indictedObjectsMapping) -> buildConstraint(constraintName, + justificationMapping) -> buildConstraint(constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, justificationMapping, - indictedObjectsMapping, stream), + stream), impactType, constraintWeight); } @@ -492,9 +490,4 @@ protected final QuadFunction, DefaultConstraintJustification> return createDefaultJustificationMapping(); } - @Override - protected final TriFunction> getDefaultIndictedObjectsMapping() { - return createDefaultIndictedObjectsMapping(); - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java index de918349a2c..b2f3f62902d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java @@ -27,7 +27,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, TriTuple< var c = tuple.getC(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a, b, c), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), constraint.getIndictedObjectsMapping(), a, b, + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, c)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java index a0021d7b035..5f8b21f8ffb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java @@ -27,11 +27,11 @@ public ScoreImpact impactWithoutJustification(WeightedScoreImpacter imp @Override public ScoreImpact impactFull(WeightedScoreImpacter impacter, TriTuple tuple) { var a = tuple.getA(); - B b = tuple.getB(); - C c = tuple.getC(); + var b = tuple.getB(); + var c = tuple.getC(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a, b, c), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), constraint.getIndictedObjectsMapping(), a, b, + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, c)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java index 144925e43a7..ac8104be37c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java @@ -5,11 +5,9 @@ import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.threeKeysGroupBy; import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.twoKeysGroupBy; import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.zeroKeysGroupBy; -import static ai.timefold.solver.core.impl.score.stream.common.uni.InnerUniConstraintStream.createDefaultIndictedObjectsMapping; import static ai.timefold.solver.core.impl.score.stream.common.uni.InnerUniConstraintStream.createDefaultJustificationMapping; import java.math.BigDecimal; -import java.util.Collection; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -491,9 +489,9 @@ private > UniConstraintBuilderImpl newTe BavetScoringConstraintStream stream, Score_ constraintWeight, ScoreImpactType impactType) { return new UniConstraintBuilderImpl<>( (constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, - justificationMapping, indictedObjectsMapping) -> buildConstraint(constraintName, + justificationMapping) -> buildConstraint(constraintName, constraintDescription, constraintGroup, constraintWeight_, impactType_, justificationMapping, - indictedObjectsMapping, stream), + stream), impactType, constraintWeight); } @@ -509,9 +507,4 @@ protected final BiFunction, DefaultConstraintJustification> getDefau return createDefaultJustificationMapping(); } - @Override - protected final Function> getDefaultIndictedObjectsMapping() { - return createDefaultIndictedObjectsMapping(); - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java index 20740660287..0da5ce44888 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java @@ -28,7 +28,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, UniTuple< var a = tuple.getA(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), constraint.getIndictedObjectsMapping(), a)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java index e86cb125116..27d8c2a6312 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java @@ -22,7 +22,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, UniTuple< var a = tuple.getA(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), constraint.getIndictedObjectsMapping(), a)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a)); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java index 59c2e3310f3..7d856de09db 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java @@ -6,8 +6,8 @@ import ai.timefold.solver.core.api.score.IBendableScore; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.definition.AbstractBendableScoreDefinition; @@ -27,7 +27,6 @@ public abstract class AbstractConstraint defaultConstraintWeight, ScoreImpactType scoreImpactType, - Object justificationMapping, Object indictedObjectsMapping) { + Object justificationMapping) { this.constraintFactory = Objects.requireNonNull(constraintFactory); this.constraintRef = Objects.requireNonNull(constraintRef); this.description = Objects.requireNonNull(description); @@ -57,7 +55,6 @@ The constraintGroup (%s) contains invalid characters. this.defaultConstraintWeight = defaultConstraintWeight; this.scoreImpactType = Objects.requireNonNull(scoreImpactType); this.justificationMapping = justificationMapping; // May be omitted in test code. - this.indictedObjectsMapping = indictedObjectsMapping; // May be omitted in test code. } @SuppressWarnings("unchecked") @@ -86,17 +83,6 @@ private > Score_ determineConstraintWeight(Solution return (Score_) defaultConstraintWeight; } - public final void assertCorrectImpact(int impact) { - if (impact >= 0) { - return; - } - if (scoreImpactType != ScoreImpactType.MIXED) { - throw new IllegalStateException("Negative match weight (" + impact + ") for constraint (" - + constraintRef + "). " + - "Check constraint provider implementation."); - } - } - public final void assertCorrectImpact(long impact) { if (impact >= 0L) { return; @@ -148,11 +134,6 @@ public JustificationMapping_ getJustificationMapping() { return (JustificationMapping_) justificationMapping; } - public IndictedObjectsMapping_ getIndictedObjectsMapping() { - // It is the job of the code constructing the constraint to ensure that this cast is correct. - return (IndictedObjectsMapping_) indictedObjectsMapping; - } - public static > void validateWeight( SolutionDescriptor solutionDescriptor, ConstraintRef constraintRef, Score_ constraintWeight) { if (constraintWeight == null) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintBuilder.java index b771185183b..6b6381b5a26 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintBuilder.java @@ -26,14 +26,11 @@ protected AbstractConstraintBuilder(ConstraintConstructor constraintConstructor, protected abstract @Nullable JustificationMapping_ getJustificationMapping(); - protected abstract @Nullable IndictedObjectsMapping_ getIndictedObjectsMapping(); - @SuppressWarnings("unchecked") @Override public final Constraint asConstraintDescribed(String constraintName, String constraintDescription, String constraintGroup) { return constraintConstructor.apply(sanitize("constraintName", constraintName), constraintDescription, - sanitize("constraintGroup", constraintGroup), constraintWeight, impactType, getJustificationMapping(), - getIndictedObjectsMapping()); + sanitize("constraintGroup", constraintGroup), constraintWeight, impactType, getJustificationMapping()); } public static String sanitize(String fieldName, String fieldValue) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStream.java index 7a12e68af60..e32f962fe3f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStream.java @@ -27,6 +27,4 @@ public RetrievalSemantics getRetrievalSemantics() { protected abstract JustificationMapping_ getDefaultJustificationMapping(); - protected abstract IndictedObjectsMapping_ getDefaultIndictedObjectsMapping(); - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java index 6429a7a8052..bc5564752f9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java @@ -34,9 +34,4 @@ protected AbstractConstraintStreamScoreDirectorFactory(SolutionDescriptor, JustificationMapping_, IndictedObjectsMapping_> { +public interface ConstraintConstructor, JustificationMapping_> { Constraint apply(String constraintName, String constraintDescription, String constraintGroup, Score_ constraintWeight, - ScoreImpactType impactType, JustificationMapping_ justificationMapping, - IndictedObjectsMapping_ indictedObjectsMapping); + ScoreImpactType impactType, JustificationMapping_ justificationMapping); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/InnerConstraintFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/InnerConstraintFactory.java index 89a53c29d55..43fd042a1d2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/InnerConstraintFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/InnerConstraintFactory.java @@ -12,10 +12,10 @@ import java.util.stream.Collectors; import ai.timefold.solver.core.api.domain.common.PlanningId; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream; import ai.timefold.solver.core.api.score.stream.bi.BiJoiner; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderImpl.java index fd14be3baba..fc4f6bb8f05 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderImpl.java @@ -1,8 +1,6 @@ package ai.timefold.solver.core.impl.score.stream.common.bi; -import java.util.Collection; import java.util.Objects; -import java.util.function.BiFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; @@ -19,7 +17,6 @@ public final class BiConstraintBuilderImpl> e implements BiConstraintBuilder { private @Nullable TriFunction justificationMapping; - private @Nullable BiFunction> indictedObjectsMapping; public BiConstraintBuilderImpl(BiConstraintConstructor constraintConstructor, ScoreImpactType impactType, Score_ constraintWeight) { @@ -44,20 +41,4 @@ Justification mapping already set (%s). return this; } - @Override - protected @Nullable BiFunction> getIndictedObjectsMapping() { - return indictedObjectsMapping; - } - - @Override - public BiConstraintBuilder indictWith(BiFunction> indictedObjectsMapping) { - if (this.indictedObjectsMapping != null) { - throw new IllegalStateException(""" - Indicted objects' mapping already set (%s). - Maybe the constraint calls indictWith() twice?""".formatted(indictedObjectsMapping)); - } - this.indictedObjectsMapping = Objects.requireNonNull(indictedObjectsMapping); - return this; - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintConstructor.java index 25f82ee61a2..d27fb914482 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintConstructor.java @@ -1,14 +1,11 @@ package ai.timefold.solver.core.impl.score.stream.common.bi; -import java.util.Collection; -import java.util.function.BiFunction; - import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.score.stream.common.ConstraintConstructor; @FunctionalInterface public interface BiConstraintConstructor> - extends ConstraintConstructor, BiFunction>> { + extends ConstraintConstructor> { } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/InnerBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/InnerBiConstraintStream.java index 4c8290512f9..60c24297ffd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/InnerBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/bi/InnerBiConstraintStream.java @@ -1,8 +1,6 @@ package ai.timefold.solver.core.impl.score.stream.common.bi; import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collection; import java.util.function.BiFunction; import java.util.function.ToLongBiFunction; @@ -25,10 +23,6 @@ static TriFunction, DefaultConstraintJustification> create return (a, b, score) -> DefaultConstraintJustification.of(score, a, b); } - static BiFunction> createDefaultIndictedObjectsMapping() { - return Arrays::asList; - } - RetrievalSemantics getRetrievalSemantics(); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java index 16720288848..ed7863f6930 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java @@ -2,20 +2,17 @@ import java.util.Collections; import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.function.Supplier; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.definition.BendableBigDecimalScoreDefinition; import ai.timefold.solver.core.impl.score.definition.BendableScoreDefinition; import ai.timefold.solver.core.impl.score.definition.HardMediumSoftBigDecimalScoreDefinition; @@ -71,8 +68,7 @@ public abstract class AbstractScoreInliner> { protected final ConstraintMatchPolicy constraintMatchPolicy; protected final Map constraintWeightMap; private final Map>> constraintMatchMap; - private @Nullable Map> constraintIdToConstraintMatchTotalMap = null; - private @Nullable Map> indictmentMap = null; + private @Nullable Map> constraintIdToConstraintMatchTotalMap = null; protected AbstractScoreInliner(Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy) { this.constraintMatchPolicy = constraintMatchPolicy; @@ -152,14 +148,13 @@ private ElementAwareLinkedList> getConstraintMatc private void clearMaps() { constraintIdToConstraintMatchTotalMap = null; - indictmentMap = null; } public ConstraintMatchPolicy getConstraintMatchPolicy() { return constraintMatchPolicy; } - public final Map> getConstraintIdToConstraintMatchTotalMap() { + public final Map> getConstraintMatchTotalMap() { if (!constraintMatchPolicy.isEnabled()) { throw new IllegalStateException("Impossible state: Method called while constraint matching is disabled."); } else if (constraintIdToConstraintMatchTotalMap == null) { @@ -169,64 +164,21 @@ public final Map> getConstraintIdToConstrai } private void rebuildConstraintMatchTotals() { - var constraintIdToConstraintMatchTotalMap = new TreeMap>(); + var constraintIdToConstraintMatchTotalMap = new TreeMap>(); for (var entry : constraintMatchMap.entrySet()) { var constraint = entry.getKey(); var constraintMatchTotal = - new DefaultConstraintMatchTotal<>(constraint.getConstraintRef(), constraintWeightMap.get(constraint)); + new ConstraintMatchTotal<>(constraint.getConstraintRef(), constraintWeightMap.get(constraint)); for (var carrier : entry.getValue()) { // Constraint match instances are only created here when we actually need them. var constraintMatch = carrier.get(); constraintMatchTotal.addConstraintMatch(constraintMatch); } - constraintIdToConstraintMatchTotalMap.put(constraint.getConstraintRef().constraintName(), constraintMatchTotal); + constraintIdToConstraintMatchTotalMap.put(constraint.getConstraintRef(), constraintMatchTotal); } this.constraintIdToConstraintMatchTotalMap = constraintIdToConstraintMatchTotalMap; } - public final Map> getIndictmentMap() { - if (!constraintMatchPolicy.isJustificationEnabled()) { - throw new IllegalStateException("Impossible state: Method called while justifications are disabled."); - } else if (indictmentMap == null) { - rebuildIndictments(); - } - return indictmentMap; - } - - private void rebuildIndictments() { - var workingIndictmentMap = new LinkedHashMap>(); - for (var entry : constraintMatchMap.entrySet()) { - for (var carrier : entry.getValue()) { - // Constraint match instances are only created here when we actually need them. - var constraintMatch = carrier.get(); - for (var indictedObject : constraintMatch.getIndictedObjectList()) { - if (indictedObject == null) { // Users may have sent null, or it came from the default mapping. - continue; - } - var indictment = getIndictment(workingIndictmentMap, constraintMatch, indictedObject); - /* - * Optimization: In order to not have to go over the indicted object list and remove duplicates, - * we use a method that will silently skip duplicate constraint matches. - * This is harmless because the two identical indicted objects come from the same constraint match. - */ - indictment.addConstraintMatchWithoutFail(constraintMatch); - } - } - } - indictmentMap = workingIndictmentMap; - } - - private DefaultIndictment getIndictment(Map> indictmentMap, - ConstraintMatch constraintMatch, Object indictedObject) { - // Like computeIfAbsent(), but doesn't create a capturing lambda on the hot path. - var indictment = (DefaultIndictment) indictmentMap.get(indictedObject); - if (indictment == null) { - indictment = new DefaultIndictment<>(indictedObject, constraintMatch.getScore().zero()); - indictmentMap.put(indictedObject, indictment); - } - return indictment; - } - public Set getConstraints() { return constraintWeightMap.keySet(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index 09b451afcbe..09c4e7115d1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -1,20 +1,17 @@ package ai.timefold.solver.core.impl.score.stream.common.inliner; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.Objects; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.stream.Collectors; import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import org.jspecify.annotations.NullMarked; @@ -22,9 +19,8 @@ * Allows creating {@link ConstraintMatch} instances lazily if and only if they are required by the end user. *

* Lazy behavior is important for constraint matching performance. - * In order to create {@link ConstraintMatch}, an entire {@link ConstraintJustification} object needs to be created, - * along with the collection of indicted objects. - * Creating these structures every time a constraint is matched would be wasteful, + * In order to create {@link ConstraintMatch}, an entire {@link ConstraintJustification} object needs to be created. + * Creating this structure every time a constraint is matched would be wasteful, * as the same constraint match can be undone almost immediately, resulting in a lot of pointless garbage. * Therefore, {@link ConstraintMatch} (along with all of its supporting data structures) * is only created when actually needed, and that is during score explanation. @@ -42,28 +38,20 @@ public interface ConstraintMatchSupplier> * @param */ static > ConstraintMatchSupplier empty() { - return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), null, Collections.emptyList(), + return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), null, impact); } static > ConstraintMatchSupplier of( BiFunction, ConstraintJustification> justificationMapping, - Function> indictedObjectsMapping, A a) { return (constraint, impact) -> { - ConstraintJustification justification; try { - justification = justificationMapping.apply(a, impact); + var justification = justificationMapping.apply(a, impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); } catch (Exception e) { throw createJustificationException(constraint, e, a); } - Collection indictedObjectCollection; - try { - indictedObjectCollection = indictedObjectsMapping.apply(a); - } catch (Exception e) { - throw createIndictmentException(constraint, e, a); - } - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, indictedObjectCollection, impact); }; } @@ -78,71 +66,42 @@ private static String factsToString(Object... facts) { .collect(Collectors.joining(", ", "{", "}")); } - private static RuntimeException createIndictmentException(Constraint constraint, Exception cause, Object... facts) { - throw new IllegalStateException("Consequence of a constraint (" + constraint.getConstraintRef() - + ") threw an exception collecting indicted objects from a tuple (" + factsToString(facts) + ").", cause); - } - static > ConstraintMatchSupplier of( TriFunction justificationMapping, - BiFunction> indictedObjectsMapping, A a, B b) { return (constraint, impact) -> { - ConstraintJustification justification; try { - justification = justificationMapping.apply(a, b, impact); + var justification = justificationMapping.apply(a, b, impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); } catch (Exception e) { throw createJustificationException(constraint, e, a, b); } - Collection indictedObjectCollection; - try { - indictedObjectCollection = indictedObjectsMapping.apply(a, b); - } catch (Exception e) { - throw createIndictmentException(constraint, e, a, b); - } - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, indictedObjectCollection, impact); }; } static > ConstraintMatchSupplier of( QuadFunction justificationMapping, - TriFunction> indictedObjectsMapping, A a, B b, C c) { return (constraint, impact) -> { - ConstraintJustification justification; try { - justification = justificationMapping.apply(a, b, c, impact); + var justification = justificationMapping.apply(a, b, c, impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); } catch (Exception e) { throw createJustificationException(constraint, e, a, b, c); } - Collection indictedObjectCollection; - try { - indictedObjectCollection = indictedObjectsMapping.apply(a, b, c); - } catch (Exception e) { - throw createIndictmentException(constraint, e, a, b, c); - } - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, indictedObjectCollection, impact); }; } static > ConstraintMatchSupplier of( PentaFunction justificationMapping, - QuadFunction> indictedObjectsMapping, A a, B b, C c, D d) { return (constraint, impact) -> { - ConstraintJustification justification; try { - justification = justificationMapping.apply(a, b, c, d, impact); + var justification = justificationMapping.apply(a, b, c, d, impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); } catch (Exception e) { throw createJustificationException(constraint, e, a, b, c, d); } - Collection indictedObjectCollection; - try { - indictedObjectCollection = indictedObjectsMapping.apply(a, b, c, d); - } catch (Exception e) { - throw createIndictmentException(constraint, e, a, b, c, d); - } - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, indictedObjectCollection, impact); }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/InnerQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/InnerQuadConstraintStream.java index 553889d8bf0..68c834fa58f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/InnerQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/InnerQuadConstraintStream.java @@ -1,8 +1,6 @@ package ai.timefold.solver.core.impl.score.stream.common.quad; import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collection; import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.function.QuadFunction; @@ -25,10 +23,6 @@ public interface InnerQuadConstraintStream extends QuadConstraintStr return (a, b, c, d, score) -> DefaultConstraintJustification.of(score, a, b, c, d); } - static QuadFunction> createDefaultIndictedObjectsMapping() { - return Arrays::asList; - } - RetrievalSemantics getRetrievalSemantics(); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderImpl.java index 1507d0bfaf5..1a0a088fc2d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderImpl.java @@ -1,10 +1,8 @@ package ai.timefold.solver.core.impl.score.stream.common.quad; -import java.util.Collection; import java.util.Objects; import ai.timefold.solver.core.api.function.PentaFunction; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintBuilder; @@ -19,7 +17,6 @@ public final class QuadConstraintBuilderImpl { private @Nullable PentaFunction justificationMapping; - private @Nullable QuadFunction> indictedObjectsMapping; public QuadConstraintBuilderImpl(QuadConstraintConstructor constraintConstructor, ScoreImpactType impactType, Score_ constraintWeight) { @@ -44,21 +41,4 @@ Justification mapping already set (%s). return this; } - @Override - protected @Nullable QuadFunction> getIndictedObjectsMapping() { - return indictedObjectsMapping; - } - - @Override - public QuadConstraintBuilder - indictWith(QuadFunction> indictedObjectsMapping) { - if (this.indictedObjectsMapping != null) { - throw new IllegalStateException(""" - Indicted objects' mapping already set (%s). - Maybe the constraint calls indictWith() twice?""".formatted(indictedObjectsMapping)); - } - this.indictedObjectsMapping = Objects.requireNonNull(indictedObjectsMapping); - return this; - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintConstructor.java index 2c4672e979b..9f9ecd49e12 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintConstructor.java @@ -1,15 +1,12 @@ package ai.timefold.solver.core.impl.score.stream.common.quad; -import java.util.Collection; - import ai.timefold.solver.core.api.function.PentaFunction; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.score.stream.common.ConstraintConstructor; @FunctionalInterface public interface QuadConstraintConstructor> extends - ConstraintConstructor, QuadFunction>> { + ConstraintConstructor> { } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/InnerTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/InnerTriConstraintStream.java index bb8d9ff0868..1604e6a6162 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/InnerTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/InnerTriConstraintStream.java @@ -1,8 +1,6 @@ package ai.timefold.solver.core.impl.score.stream.common.tri; import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collection; import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.ToLongTriFunction; @@ -25,10 +23,6 @@ static QuadFunction, DefaultConstraintJustification> return (a, b, c, score) -> DefaultConstraintJustification.of(score, a, b, c); } - static TriFunction> createDefaultIndictedObjectsMapping() { - return Arrays::asList; - } - RetrievalSemantics getRetrievalSemantics(); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderImpl.java index d4f284b1ee9..6e0483c9dd4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderImpl.java @@ -1,10 +1,8 @@ package ai.timefold.solver.core.impl.score.stream.common.tri; -import java.util.Collection; import java.util.Objects; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintBuilder; @@ -19,7 +17,6 @@ public final class TriConstraintBuilderImpl { private @Nullable QuadFunction justificationMapping; - private @Nullable TriFunction> indictedObjectsMapping; public TriConstraintBuilderImpl(TriConstraintConstructor constraintConstructor, ScoreImpactType impactType, Score_ constraintWeight) { @@ -44,20 +41,4 @@ Justification mapping already set (%s). return this; } - @Override - protected @Nullable TriFunction> getIndictedObjectsMapping() { - return indictedObjectsMapping; - } - - @Override - public TriConstraintBuilder indictWith(TriFunction> indictedObjectsMapping) { - if (this.indictedObjectsMapping != null) { - throw new IllegalStateException(""" - Indicted objects' mapping already set (%s). - Maybe the constraint calls indictWith() twice?""".formatted(indictedObjectsMapping)); - } - this.indictedObjectsMapping = Objects.requireNonNull(indictedObjectsMapping); - return this; - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintConstructor.java index 976be21e0a7..a0a996c95b1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintConstructor.java @@ -1,14 +1,11 @@ package ai.timefold.solver.core.impl.score.stream.common.tri; -import java.util.Collection; - import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.score.stream.common.ConstraintConstructor; @FunctionalInterface public interface TriConstraintConstructor> - extends ConstraintConstructor, TriFunction>> { + extends ConstraintConstructor> { } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/InnerUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/InnerUniConstraintStream.java index d66ad87ae96..62277eedbaa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/InnerUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/InnerUniConstraintStream.java @@ -1,8 +1,6 @@ package ai.timefold.solver.core.impl.score.stream.common.uni; import java.math.BigDecimal; -import java.util.Collection; -import java.util.Collections; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.ToLongFunction; @@ -27,10 +25,6 @@ static BiFunction, DefaultConstraintJustification> createDefault return (a, score) -> DefaultConstraintJustification.of(score, a); } - static Function> createDefaultIndictedObjectsMapping() { - return Collections::singletonList; - } - RetrievalSemantics getRetrievalSemantics(); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderImpl.java index c96fd7b63e2..d0cccd0db92 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderImpl.java @@ -1,9 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.uni; -import java.util.Collection; import java.util.Objects; import java.util.function.BiFunction; -import java.util.function.Function; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; @@ -19,7 +17,6 @@ public final class UniConstraintBuilderImpl> ext implements UniConstraintBuilder { private @Nullable BiFunction justificationMapping; - private @Nullable Function> indictedObjectsMapping; public UniConstraintBuilderImpl(UniConstraintConstructor constraintConstructor, ScoreImpactType impactType, Score_ constraintWeight) { @@ -44,20 +41,4 @@ Justification mapping already set (%s). return this; } - @Override - protected @Nullable Function> getIndictedObjectsMapping() { - return indictedObjectsMapping; - } - - @Override - public UniConstraintBuilder indictWith(Function> indictedObjectsMapping) { - if (this.indictedObjectsMapping != null) { - throw new IllegalStateException(""" - Indicted objects' mapping already set (%s). - Maybe the constraint calls indictWith() twice?""".formatted(indictedObjectsMapping)); - } - this.indictedObjectsMapping = Objects.requireNonNull(indictedObjectsMapping); - return this; - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintConstructor.java index 97e37691b52..a915fd2dac6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintConstructor.java @@ -1,14 +1,12 @@ package ai.timefold.solver.core.impl.score.stream.common.uni; -import java.util.Collection; import java.util.function.BiFunction; -import java.util.function.Function; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.score.stream.common.ConstraintConstructor; @FunctionalInterface public interface UniConstraintConstructor> - extends ConstraintConstructor, Function>> { + extends ConstraintConstructor> { } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractConstraintAssertion.java index 403af571c36..7ba21c1bab3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractConstraintAssertion.java @@ -1,11 +1,15 @@ package ai.timefold.solver.core.impl.score.stream.test; +import java.util.Collection; import java.util.Map; +import java.util.TreeMap; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; +import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; @@ -22,8 +26,7 @@ protected AbstractConstraintAssertion( abstract Solution_ getSolution(); - abstract void update(InnerScore score, Map> constraintMatchTotalMap, - Map> indictmentMap); + abstract void update(InnerScore score, Map> constraintMatchTotalMap); /** * The logic ensures the solution is initialized only once. @@ -64,8 +67,7 @@ void ensureInitialized() { if (bavetConstraintStreamScoreDirector != null) { bavetConstraintStreamScoreDirector.clearShadowVariablesListenerQueue(); } - update(scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap(), - scoreDirector.getIndictmentMap()); + update(scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap()); initialized = true; } } @@ -73,4 +75,19 @@ void ensureInitialized() { void toggleInitialized() { this.initialized = true; } + + protected String explainScore(InnerScore workingScore, + Collection> constraintMatchTotalCollection) { + return TimefoldSolverEnterpriseService.loadOrDefault( + s -> { + var constraintAnalyses = new TreeMap>(); + for (var constraintMatchTotal : constraintMatchTotalCollection) { + var constraintRef = constraintMatchTotal.getConstraintRef(); + constraintAnalyses.put(constraintRef, constraintMatchTotal); + } + return s.analyze(workingScore, constraintAnalyses, ScoreAnalysisFetchPolicy.FETCH_ALL) + .summarize(); + }, + () -> "Score analysis is only available in Timefold Solver Enterprise Edition."); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractMultiConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractMultiConstraintAssertion.java index fe656e62d7e..ad2c346b61a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractMultiConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractMultiConstraintAssertion.java @@ -6,11 +6,10 @@ import java.util.Map; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.test.MultiConstraintAssertion; -import ai.timefold.solver.core.impl.score.DefaultScoreExplanation; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; @@ -24,7 +23,6 @@ public abstract sealed class AbstractMultiConstraintAssertion actualScore; private Collection> constraintMatchTotalCollection; - private Collection> indictmentCollection; AbstractMultiConstraintAssertion(ConstraintProvider constraintProvider, AbstractConstraintStreamScoreDirectorFactory scoreDirectorFactory) { @@ -33,11 +31,9 @@ public abstract sealed class AbstractMultiConstraintAssertion innerScore, Map> constraintMatchTotalMap, - Map> indictmentMap) { + final void update(InnerScore innerScore, Map> constraintMatchTotalMap) { this.actualScore = InnerScore.fullyAssigned(requireNonNull(innerScore).raw()); // Strip initialization information. this.constraintMatchTotalCollection = requireNonNull(constraintMatchTotalMap).values(); - this.indictmentCollection = requireNonNull(indictmentMap).values(); toggleInitialized(); } @@ -58,8 +54,7 @@ public void scores(@NonNull Score score, String message) { %s""" .formatted(expectation, constraintProviderClass, score, score.getClass(), actualScore, actualScore.getClass(), - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, - indictmentCollection))); + explainScore(actualScore, constraintMatchTotalCollection))); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractSingleConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractSingleConstraintAssertion.java index a1351ccc702..4c86bf3844d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractSingleConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/AbstractSingleConstraintAssertion.java @@ -13,11 +13,10 @@ import java.util.stream.Stream; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.test.SingleConstraintAssertion; -import ai.timefold.solver.core.impl.score.DefaultScoreExplanation; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; @@ -38,7 +37,6 @@ public abstract sealed class AbstractSingleConstraintAssertion actualScore; private Collection> constraintMatchTotalCollection; private Collection justificationCollection; - private Collection> indictmentCollection; @SuppressWarnings("unchecked") AbstractSingleConstraintAssertion(AbstractConstraintStreamScoreDirectorFactory scoreDirectorFactory) { @@ -52,11 +50,9 @@ public abstract sealed class AbstractSingleConstraintAssertion innerScore, Map> constraintMatchTotalMap, - Map> indictmentMap) { + final void update(InnerScore innerScore, Map> constraintMatchTotalMap) { this.actualScore = InnerScore.fullyAssigned(requireNonNull(innerScore).raw()); // Strip initialization information. this.constraintMatchTotalCollection = new ArrayList<>(requireNonNull(constraintMatchTotalMap).values()); - this.indictmentCollection = new ArrayList<>(requireNonNull(indictmentMap).values()); this.justificationCollection = this.constraintMatchTotalCollection.stream() .flatMap(c -> c.getConstraintMatchSet().stream()) .map(c -> (ConstraintJustification) c.getJustification()) @@ -73,13 +69,6 @@ final void update(InnerScore innerScore, Map innerScore, Map(indictmentObjectList.size()); - for (var indictment : indictments) { - // Test invalid match - if (indictmentObjectList.stream().noneMatch(indictment::equals)) { - expectedNotFound.add(indictment); - } - } - var unexpectedFound = emptyList(); - if (completeValidation) { - unexpectedFound = indictmentObjectList.stream() - .filter(indictment -> Arrays.stream(indictments).noneMatch(indictment::equals)) - .toList(); - } - if (expectedNotFound.isEmpty() && unexpectedFound.isEmpty()) { - return; - } - var assertionMessage = buildAssertionErrorMessage("Indictment", constraint.getConstraintRef().constraintName(), - unexpectedFound, expectedNotFound, Arrays.asList(indictments), indictmentObjectList, message); - throw new AssertionError(assertionMessage); - } - /** * Returns sum total of constraint match impacts, * deduced from constraint matches. @@ -595,7 +533,7 @@ private String buildAssertionErrorMessage(ScoreImpactType expectedImpactType, Nu "Constraint", constraintId, expectedImpactLabel, expectedImpact, expectedImpact.getClass(), actualImpactLabel, actualImpact, actualImpact.getClass(), - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, indictmentCollection)); + explainScore(actualScore, constraintMatchTotalCollection)); } private String buildMoreThanAssertionErrorMessage(ScoreImpactType expectedImpactType, Number expectedImpact, @@ -621,7 +559,7 @@ private String buildMoreOrLessThanAssertionErrorMessage(ScoreImpactType expected "Constraint", constraintId, expectedImpactLabel, expectedImpact, expectedImpact.getClass(), actualImpactLabel, actualImpact, actualImpact.getClass(), - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, indictmentCollection)); + explainScore(actualScore, constraintMatchTotalCollection)); } private String buildAssertionErrorMessage(ScoreImpactType impactType, long expectedTimes, long actualTimes, @@ -635,7 +573,7 @@ private String buildAssertionErrorMessage(ScoreImpactType impactType, long expec "Constraint", constraintId, expectedImpactLabel, expectedTimes, actualImpactLabel, actualTimes, - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, indictmentCollection)); + explainScore(actualScore, constraintMatchTotalCollection)); } private String buildMoreThanAssertionErrorMessage(ScoreImpactType impactType, long expectedTimes, long actualTimes, @@ -662,7 +600,7 @@ private String buildMoreOrLessThanAssertionErrorMessage(ScoreImpactType impactTy "Constraint", constraintId, expectedImpactLabel, expectedTimes, actualImpactLabel, actualTimes, - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, indictmentCollection)); + explainScore(actualScore, constraintMatchTotalCollection)); } private String buildAssertionErrorMessage(ScoreImpactType impactType, String constraintId, String message) { @@ -673,7 +611,7 @@ private String buildAssertionErrorMessage(ScoreImpactType impactType, String con expectation, "Constraint", constraintId, expectedImpactLabel, - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, indictmentCollection)); + explainScore(actualScore, constraintMatchTotalCollection)); } private static String buildAssertionErrorMessage(String type, String constraintId, Collection unexpectedFound, @@ -745,7 +683,7 @@ private String buildNoImpactAssertionErrorMessage(Number actualImpact, String co expectation, constraintId, actualImpact, actualImpact.getClass(), - DefaultScoreExplanation.explainScore(actualScore, constraintMatchTotalCollection, indictmentCollection)); + explainScore(actualScore, constraintMatchTotalCollection)); } private static String getImpactTypeLabel(ScoreImpactType scoreImpactType) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ConfiguredConstraintVerifier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ConfiguredConstraintVerifier.java index 3601c110180..0fc741238fb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ConfiguredConstraintVerifier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ConfiguredConstraintVerifier.java @@ -6,10 +6,10 @@ import java.util.function.BiFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.test.ConstraintVerifier; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultMultiConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultMultiConstraintAssertion.java index e57bef622a4..40db42efe28 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultMultiConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultMultiConstraintAssertion.java @@ -3,9 +3,9 @@ import java.util.Map; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; @@ -14,10 +14,9 @@ public final class DefaultMultiConstraintAssertion scoreDirectorFactory, Score_ actualScore, - Map> constraintMatchTotalMap, - Map> indictmentMap) { + Map> constraintMatchTotalMap) { super(constraintProvider, scoreDirectorFactory); - update(InnerScore.fullyAssigned(actualScore), constraintMatchTotalMap, indictmentMap); + update(InnerScore.fullyAssigned(actualScore), constraintMatchTotalMap); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareMultiConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareMultiConstraintAssertion.java index 18311aad1f9..30c57c6e7d3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareMultiConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareMultiConstraintAssertion.java @@ -32,8 +32,7 @@ public MultiConstraintAssertion settingAllShadowVariables() { .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) .buildDerived()) { scoreDirector.setWorkingSolution(solution); - update(scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap(), - scoreDirector.getIndictmentMap()); + update(scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap()); toggleInitialized(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareSingleConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareSingleConstraintAssertion.java index 55b9dbb64f6..6960cdd7299 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareSingleConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultShadowVariableAwareSingleConstraintAssertion.java @@ -29,8 +29,7 @@ public SingleConstraintAssertion settingAllShadowVariables() { .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) .buildDerived()) { scoreDirector.setWorkingSolution(solution); - update(scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap(), - scoreDirector.getIndictmentMap()); + update(scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap()); toggleInitialized(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultSingleConstraintAssertion.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultSingleConstraintAssertion.java index da57ff2146e..423365f881f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultSingleConstraintAssertion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/DefaultSingleConstraintAssertion.java @@ -3,8 +3,8 @@ import java.util.Map; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; @@ -12,10 +12,9 @@ public final class DefaultSingleConstraintAssertion { DefaultSingleConstraintAssertion(AbstractConstraintStreamScoreDirectorFactory scoreDirectorFactory, - Score_ score, Map> constraintMatchTotalMap, - Map> indictmentMap) { + Score_ score, Map> constraintMatchTotalMap) { super(scoreDirectorFactory); - update(InnerScore.fullyAssigned(score), constraintMatchTotalMap, indictmentMap); + update(InnerScore.fullyAssigned(score), constraintMatchTotalMap); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ScoreDirectorFactoryCache.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ScoreDirectorFactoryCache.java index f14a2428f48..ec3c186b71b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ScoreDirectorFactoryCache.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/ScoreDirectorFactoryCache.java @@ -7,10 +7,10 @@ import java.util.function.BiFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; @@ -82,8 +82,7 @@ public ScoreDirectorFactoryCache(SolutionDescriptor solutionDescripto * @return never null */ public AbstractConstraintStreamScoreDirectorFactory getScoreDirectorFactory( - ConstraintRef constraintRef, - ConstraintProvider constraintProvider, EnvironmentMode environmentMode) { + ConstraintRef constraintRef, ConstraintProvider constraintProvider, EnvironmentMode environmentMode) { return scoreDirectorFactoryMap.computeIfAbsent(constraintRef, k -> new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, environmentMode)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/SessionBasedAssertionBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/SessionBasedAssertionBuilder.java index 2b93b4341d3..9191d74215c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/SessionBasedAssertionBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/test/SessionBasedAssertionBuilder.java @@ -19,15 +19,13 @@ public DefaultMultiConstraintAssertion multiConstraintGiven(C Object... facts) { var scoreInliner = constraintStreamScoreDirectorFactory.fireAndForget(facts); return new DefaultMultiConstraintAssertion<>(constraintProvider, constraintStreamScoreDirectorFactory, - scoreInliner.extractScore(), scoreInliner.getConstraintIdToConstraintMatchTotalMap(), - scoreInliner.getIndictmentMap()); + scoreInliner.extractScore(), scoreInliner.getConstraintMatchTotalMap()); } public DefaultSingleConstraintAssertion singleConstraintGiven(Object... facts) { var scoreInliner = constraintStreamScoreDirectorFactory.fireAndForget(facts); return new DefaultSingleConstraintAssertion<>(constraintStreamScoreDirectorFactory, - scoreInliner.extractScore(), scoreInliner.getConstraintIdToConstraintMatchTotalMap(), - scoreInliner.getIndictmentMap()); + scoreInliner.extractScore(), scoreInliner.getConstraintMatchTotalMap()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java deleted file mode 100644 index f895c9654d7..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java +++ /dev/null @@ -1,54 +0,0 @@ -package ai.timefold.solver.core.impl.solver; - -import java.util.List; -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -final class Assigner, Recommendation_, In_, Out_> - implements Function, List> { - - private final DefaultSolverFactory solverFactory; - private final Function propositionFunction; - private final RecommendationConstructor recommendationConstructor; - private final ScoreAnalysisFetchPolicy fetchPolicy; - private final Solution_ originalSolution; - private final In_ originalElement; - - public Assigner(DefaultSolverFactory solverFactory, Function propositionFunction, - RecommendationConstructor recommendationConstructor, - ScoreAnalysisFetchPolicy fetchPolicy, Solution_ originalSolution, In_ originalElement) { - this.solverFactory = Objects.requireNonNull(solverFactory); - this.propositionFunction = Objects.requireNonNull(propositionFunction); - this.recommendationConstructor = Objects.requireNonNull(recommendationConstructor); - this.fetchPolicy = Objects.requireNonNull(fetchPolicy); - this.originalSolution = Objects.requireNonNull(originalSolution); - this.originalElement = Objects.requireNonNull(originalElement); - } - - @Override - public List apply(InnerScoreDirector scoreDirector) { - var initializationStatistics = scoreDirector.getValueRangeManager().getInitializationStatistics(); - var uninitializedCount = - initializationStatistics.uninitializedEntityCount() + initializationStatistics.unassignedValueCount(); - if (uninitializedCount > 1) { - throw new IllegalStateException(""" - Solution (%s) has (%d) uninitialized elements. - Assignment Recommendation API requires at most one uninitialized element in the solution.""" - .formatted(originalSolution, uninitializedCount)); - } - var originalScoreAnalysis = scoreDirector.buildScoreAnalysis(fetchPolicy); - var clonedElement = scoreDirector.lookUpWorkingObject(originalElement); - var processor = new AssignmentProcessor<>(solverFactory, propositionFunction, recommendationConstructor, fetchPolicy, - clonedElement, originalScoreAnalysis); - return processor.apply(scoreDirector); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java deleted file mode 100644 index 1bb3344d73b..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java +++ /dev/null @@ -1,151 +0,0 @@ -package ai.timefold.solver.core.impl.solver; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Random; -import java.util.function.Function; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; -import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase; -import ai.timefold.solver.core.impl.constructionheuristic.placer.EntityPlacer; -import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; -import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope; -import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; -import ai.timefold.solver.core.impl.heuristic.selector.move.generic.SelectorBasedChangeMove; -import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.SelectorBasedListUnassignMove; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; -import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; -import ai.timefold.solver.core.preview.api.move.Move; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -final class AssignmentProcessor, Recommendation_, In_, Out_> - implements Function, List> { - - private final DefaultSolverFactory solverFactory; - private final Function propositionFunction; - private final RecommendationConstructor recommendationConstructor; - private final ScoreAnalysisFetchPolicy fetchPolicy; - private final ScoreAnalysis originalScoreAnalysis; - private final In_ clonedElement; - - public AssignmentProcessor(DefaultSolverFactory solverFactory, Function propositionFunction, - RecommendationConstructor recommendationConstructor, - ScoreAnalysisFetchPolicy fetchPolicy, In_ clonedElement, ScoreAnalysis originalScoreAnalysis) { - this.solverFactory = Objects.requireNonNull(solverFactory); - this.propositionFunction = Objects.requireNonNull(propositionFunction); - this.recommendationConstructor = Objects.requireNonNull(recommendationConstructor); - this.fetchPolicy = Objects.requireNonNull(fetchPolicy); - this.originalScoreAnalysis = Objects.requireNonNull(originalScoreAnalysis); - this.clonedElement = clonedElement; - } - - @Override - public List apply(InnerScoreDirector scoreDirector) { - // The cloned element may already be assigned. - // If it is, we need to unassign it before we can run the construction heuristic. - var moveDirector = scoreDirector.getMoveDirector(); - var supplyManager = scoreDirector.getSupplyManager(); - var solutionDescriptor = solverFactory.getSolutionDescriptor(); - var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor(); - if (listVariableDescriptor != null) { - var demand = listVariableDescriptor.getStateDemand(); - try (var listVariableStateSupply = supplyManager.demand(demand)) { - var elementPosition = listVariableStateSupply.getElementPosition(clonedElement); - if (elementPosition instanceof PositionInList positionInList) { // Unassign the cloned element. - var entity = positionInList.entity(); - var index = positionInList.index(); - moveDirector.execute(new SelectorBasedListUnassignMove<>(listVariableDescriptor, entity, index)); - } - } - } else { - var entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(clonedElement.getClass()); - for (var variableDescriptor : entityDescriptor.getGenuineVariableDescriptorList()) { - var basicVariableDescriptor = (BasicVariableDescriptor) variableDescriptor; - if (basicVariableDescriptor.getValue(clonedElement) == null) { - // The variable is already unassigned. - continue; - } - // Uninitialize the basic variable. - moveDirector.execute(new SelectorBasedChangeMove<>(basicVariableDescriptor, clonedElement, null)); - } - } - scoreDirector.triggerVariableListeners(); - - // The placers needs to be filtered. - // If anything else than the cloned element is unassigned, we want to keep it unassigned. - // Otherwise the solution would have to explicitly pin everything other than the cloned element. - var entityPlacer = buildEntityPlacer() - .rebuildWithFilter((solution, selection) -> selection == clonedElement); - - var solverScope = new SolverScope(solverFactory.getClock()); - solverScope.setWorkingRandom(new Random(0)); // We will evaluate every option; random does not matter. - solverScope.setScoreDirector(scoreDirector); - var phaseScope = new ConstructionHeuristicPhaseScope<>(solverScope, -1); - var stepScope = new ConstructionHeuristicStepScope<>(phaseScope); - entityPlacer.solvingStarted(solverScope); - entityPlacer.phaseStarted(phaseScope); - entityPlacer.stepStarted(stepScope); - - try (scoreDirector) { - var placementIterator = entityPlacer.iterator(); - if (!placementIterator.hasNext()) { - throw new IllegalStateException(""" - Impossible state: entity placer (%s) has no placements. - """.formatted(entityPlacer)); - } - var placement = placementIterator.next(); - var recommendedAssignmentList = new ArrayList(); - var moveIndex = 0L; - for (var move : placement) { - recommendedAssignmentList.add(execute(scoreDirector, move, moveIndex, clonedElement, propositionFunction)); - moveIndex++; - } - recommendedAssignmentList.sort(null); - return recommendedAssignmentList; - } finally { - entityPlacer.stepEnded(stepScope); - entityPlacer.phaseEnded(phaseScope); - entityPlacer.solvingEnded(solverScope); - } - } - - private EntityPlacer buildEntityPlacer() { - var solver = (DefaultSolver) solverFactory.buildSolver(); - var phaseList = solver.getPhaseList(); - var constructionHeuristicCount = phaseList.stream() - .filter(DefaultConstructionHeuristicPhase.class::isInstance) - .count(); - if (constructionHeuristicCount != 1) { - throw new IllegalStateException( - "Assignment Recommendation API requires the solver config to have exactly one construction heuristic phase, but it has (%s) instead." - .formatted(constructionHeuristicCount)); - } - var phase = phaseList.get(0); - if (phase instanceof DefaultConstructionHeuristicPhase constructionHeuristicPhase) { - return constructionHeuristicPhase.getEntityPlacer(); - } else { - throw new IllegalStateException( - "Assignment Recommendation API requires the first solver phase (%s) in the solver config to be a construction heuristic." - .formatted(phase)); - } - } - - private Recommendation_ execute(InnerScoreDirector scoreDirector, Move move, long moveIndex, - In_ clonedElement, Function propositionFunction) { - return Objects.requireNonNull(scoreDirector.getMoveDirector() - .executeTemporary(move, (unused, unused2) -> { - var newScoreAnalysis = scoreDirector.buildScoreAnalysis(fetchPolicy); - var newScoreDifference = newScoreAnalysis.diff(originalScoreAnalysis); - return recommendationConstructor.apply(moveIndex, propositionFunction.apply(clonedElement), - newScoreDifference); - })); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultRecommendedAssignment.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultRecommendedAssignment.java deleted file mode 100644 index 9f7dff17440..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultRecommendedAssignment.java +++ /dev/null @@ -1,27 +0,0 @@ -package ai.timefold.solver.core.impl.solver; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -public record DefaultRecommendedAssignment>(long index, - @Nullable Proposition_ proposition, ScoreAnalysis scoreAnalysisDiff) - implements - RecommendedAssignment, - Comparable> { - - @Override - public int compareTo(DefaultRecommendedAssignment other) { - int scoreComparison = scoreAnalysisDiff.score().compareTo(other.scoreAnalysisDiff.score()); - if (scoreComparison != 0) { - return -scoreComparison; // Better scores first. - } - // Otherwise maintain insertion order. - return Long.compareUnsigned(index, other.index); // Unsigned == many more positive values. - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java index d1a066762db..34b3c0049f0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java @@ -6,7 +6,6 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.ScoreExplanation; import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; import ai.timefold.solver.core.api.solver.RecommendedAssignment; import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; @@ -16,9 +15,9 @@ import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.PreviewFeature; +import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.domain.variable.declarative.ConsistencyTracker; import ai.timefold.solver.core.impl.domain.variable.listener.support.violation.VariableSnapshotTotal; -import ai.timefold.solver.core.impl.score.DefaultScoreExplanation; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; @@ -62,16 +61,14 @@ public Score_ update(Solution_ solution, SolutionUpdatePolicy solutionUpdatePoli s -> s.getSolutionDescriptor().getScore(s.getWorkingSolution()), ConstraintMatchPolicy.DISABLED, false); } - private Result_ callScoreDirector(Solution_ solution, - SolutionUpdatePolicy solutionUpdatePolicy, Function, Result_> function, - ConstraintMatchPolicy constraintMatchPolicy, boolean cloneSolution) { + private Result_ callScoreDirector(Solution_ solution, SolutionUpdatePolicy solutionUpdatePolicy, + Function, Result_> function, ConstraintMatchPolicy constraintMatchPolicy, + boolean cloneSolution) { var isShadowVariableUpdateEnabled = solutionUpdatePolicy.isShadowVariableUpdateEnabled(); var nonNullSolution = Objects.requireNonNull(solution); - try (var scoreDirector = getScoreDirectorFactory().createScoreDirectorBuilder() - .withLookUpEnabled(cloneSolution) + try (var scoreDirector = getScoreDirectorFactory().createScoreDirectorBuilder().withLookUpEnabled(cloneSolution) .withConstraintMatchPolicy(constraintMatchPolicy) - .withExpectShadowVariablesInCorrectState(!isShadowVariableUpdateEnabled) - .build()) { + .withExpectShadowVariablesInCorrectState(!isShadowVariableUpdateEnabled).build()) { nonNullSolution = cloneSolution ? scoreDirector.cloneSolution(nonNullSolution) : nonNullSolution; if (isShadowVariableUpdateEnabled) { scoreDirector.setWorkingSolution(nonNullSolution); @@ -94,17 +91,6 @@ private Result_ callScoreDirector(Solution_ solution, } } - @SuppressWarnings("unchecked") - @Override - public ScoreExplanation explain(Solution_ solution, - SolutionUpdatePolicy solutionUpdatePolicy) { - var currentScore = (Score_) scoreDirectorFactory.getSolutionDescriptor().getScore(solution); - var explanation = callScoreDirector(solution, solutionUpdatePolicy, DefaultScoreExplanation::new, - ConstraintMatchPolicy.ENABLED, false); - assertFreshScore(solution, currentScore, explanation.getScore(), solutionUpdatePolicy); - return explanation; - } - private void assertFreshScore(Solution_ solution, Score_ currentScore, Score_ calculatedScore, SolutionUpdatePolicy solutionUpdatePolicy) { if (!solutionUpdatePolicy.isScoreUpdateEnabled()) { @@ -115,10 +101,8 @@ private void assertFreshScore(Solution_ solution, Score_ currentScore, Score_ ca Current score (%s) and freshly calculated score (%s) for solution (%s) do not match. Maybe run %s environment mode to check for score corruptions. Otherwise enable %s.%s to update the stale score. - """ - .formatted(currentScore, calculatedScore, solution, EnvironmentMode.TRACKED_FULL_ASSERT, - SolutionUpdatePolicy.class.getSimpleName(), - SolutionUpdatePolicy.UPDATE_ALL)); + """.formatted(currentScore, calculatedScore, solution, EnvironmentMode.TRACKED_FULL_ASSERT, + SolutionUpdatePolicy.class.getSimpleName(), SolutionUpdatePolicy.UPDATE_ALL)); } } } @@ -128,29 +112,35 @@ Current score (%s) and freshly calculated score (%s) for solution (%s) do not ma public ScoreAnalysis analyze(Solution_ solution, ScoreAnalysisFetchPolicy fetchPolicy, SolutionUpdatePolicy solutionUpdatePolicy) { Objects.requireNonNull(fetchPolicy, "fetchPolicy"); + var enterpriseService = + TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.SCORE_ANALYSIS); var currentScore = (Score_) scoreDirectorFactory.getSolutionDescriptor().getScore(solution); var analysis = callScoreDirector(solution, solutionUpdatePolicy, - scoreDirector -> scoreDirector.buildScoreAnalysis(fetchPolicy), ConstraintMatchPolicy.match(fetchPolicy), - false); + scoreDirector -> enterpriseService.analyze(scoreDirector.calculateScore(), + scoreDirector.getConstraintMatchTotalMap(), fetchPolicy), + ConstraintMatchPolicy.match(fetchPolicy), false); assertFreshScore(solution, currentScore, analysis.score(), solutionUpdatePolicy); return analysis; } @Override public PlanningSolutionDiff diff(Solution_ oldSolution, Solution_ newSolution) { + var enterpriseService = + TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.RECOMMENDATIONS); solverFactory.ensurePreviewFeature(PreviewFeature.PLANNING_SOLUTION_DIFF); - return solverFactory.getSolutionDescriptor() - .diff(oldSolution, newSolution); + return enterpriseService.solutionDiff(solverFactory.getSolutionDescriptor().getMetaModel(), oldSolution, newSolution); } @Override public List> recommendAssignment(Solution_ solution, In_ evaluatedEntityOrElement, Function propositionFunction, ScoreAnalysisFetchPolicy fetchPolicy) { - var assigner = new Assigner, In_, Out_>(solverFactory, - propositionFunction, DefaultRecommendedAssignment::new, fetchPolicy, solution, evaluatedEntityOrElement); - return callScoreDirector(solution, SolutionUpdatePolicy.UPDATE_ALL, assigner, ConstraintMatchPolicy.match(fetchPolicy), - true); + var enterpriseService = + TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.RECOMMENDATIONS); + return callScoreDirector( + solution, SolutionUpdatePolicy.UPDATE_ALL, enterpriseService.buildRecommender(solverFactory, solution, + evaluatedEntityOrElement, propositionFunction, fetchPolicy), + ConstraintMatchPolicy.match(fetchPolicy), true); } /** @@ -161,15 +151,15 @@ public List> recommendAssignment *
      * $ dot -Tsvg input.dot > output.svg
      * 
- * + * * This assumes the string returned by this method is saved to a file named {@code input.dot}. - * + * *

* The node network itself is an internal implementation detail of Constraint Streams. * Do not rely on any particular node network structure in production code, * and do not micro-optimize your constraints to match the node network. * Such optimizations are destined to become obsolete and possibly harmful as the node network evolves. - * + * *

* This method is only provided for debugging purposes * and is deliberately not part of the public API. @@ -184,8 +174,8 @@ public List> recommendAssignment public @Nullable String visualizeNodeNetwork(Solution_ solution) { if (scoreDirectorFactory instanceof BavetConstraintStreamScoreDirectorFactory bavetScoreDirectorFactory) { var result = new MutableReference(null); - bavetScoreDirectorFactory.newSession(solution, new ConsistencyTracker<>(), - ConstraintMatchPolicy.ENABLED, false, result::setValue); + bavetScoreDirectorFactory.newSession(solution, new ConsistencyTracker<>(), ConstraintMatchPolicy.ENABLED, false, + result::setValue); return result.getValue(); } throw new UnsupportedOperationException("Node network visualization is only supported when using Constraint Streams."); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/RecommendationConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/RecommendationConstructor.java deleted file mode 100644 index d935ca5d927..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/RecommendationConstructor.java +++ /dev/null @@ -1,14 +0,0 @@ -package ai.timefold.solver.core.impl.solver; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -interface RecommendationConstructor, Recommendation_, Out_> { - - Recommendation_ apply(long moveIndex, @Nullable Out_ result, ScoreAnalysis scoreDifference); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningEntityDiff.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningEntityDiff.java index f246c410635..e0ac99e3170 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningEntityDiff.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningEntityDiff.java @@ -16,6 +16,9 @@ * This interface is not intended to be implemented by users. * The default implementation has a default {@code toString()} method that prints a summary of the differences. * Do not attempt to parse that string - it is subject to change in the future. + *

+ * Note: Solution diff is exclusive to Timefold Solver Enterprise Edition. + * This interface has no use without the Enterprise Edition. * * @param * @param diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningSolutionDiff.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningSolutionDiff.java index e40b1b2763f..af90da19438 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningSolutionDiff.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningSolutionDiff.java @@ -19,6 +19,9 @@ * This interface is not intended to be implemented by users. * The default implementation has a default {@code toString()} method that prints a summary of the differences. * Do not attempt to parse that string - it is subject to change in the future. + *

+ * Note: Solution diff is exclusive to Timefold Solver Enterprise Edition. + * This interface has no use without the Enterprise Edition. * * @param */ diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningVariableDiff.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningVariableDiff.java index 5e5ebc178a9..df369dff6f0 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningVariableDiff.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/PlanningVariableDiff.java @@ -13,6 +13,9 @@ * This interface is not intended to be implemented by users. * The default implementation has a default {@code toString()} method that prints a summary of the differences. * Do not attempt to parse that string - it is subject to change in the future. + *

+ * Note: Solution diff is exclusive to Timefold Solver Enterprise Edition. + * This interface has no use without the Enterprise Edition. * * @param * @param diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/package-info.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/package-info.java index aa9c97eab1f..e7b7006c403 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/package-info.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/solution/diff/package-info.java @@ -4,6 +4,9 @@ * using the {@link ai.timefold.solver.core.api.solver.SolutionManager#diff(java.lang.Object, java.lang.Object)}. * *

+ * Note: Solution diff is exclusive to Timefold Solver Enterprise Edition. + * + *

* This package and all of its contents is only offered as a preview feature. * There are no guarantees for backward compatibility; * any class, method, or field may change or be removed without prior notice, diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 1d8579870cc..ed35977a9ca 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -11,7 +11,6 @@ exports ai.timefold.solver.core.api.function; exports ai.timefold.solver.core.api.score; exports ai.timefold.solver.core.api.score.analysis; - exports ai.timefold.solver.core.api.score.constraint; exports ai.timefold.solver.core.api.score.stream; exports ai.timefold.solver.core.api.score.stream.common; exports ai.timefold.solver.core.api.score.stream.uni; @@ -151,8 +150,6 @@ ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.score.director.easy to ai.timefold.solver.benchmark, ai.timefold.solver.enterprise.core; - exports ai.timefold.solver.core.impl.score.director.incremental to ai.timefold.solver.benchmark, - ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.score.stream.common to ai.timefold.solver.quarkus, ai.timefold.solver.spring.boot.autoconfigure; exports ai.timefold.solver.core.impl.score.stream.collector @@ -168,7 +165,8 @@ ai.timefold.solver.quarkus, ai.timefold.solver.quarkus.deployment, ai.timefold.solver.quarkus.integration.test, ai.timefold.solver.quarkus.jackson, - ai.timefold.solver.enterprise.core; + ai.timefold.solver.enterprise.core, ai.timefold.solver.enterprise.jackson, + ai.timefold.solver.enterprise.quarkus.jackson; exports ai.timefold.solver.core.impl.solver.monitoring to ai.timefold.solver.benchmark, ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.solver.scope to @@ -184,8 +182,10 @@ // enterprise-specific exports exports ai.timefold.solver.core.impl.bavet.common to ai.timefold.solver.enterprise.core; + exports ai.timefold.solver.core.impl.constructionheuristic to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.constructionheuristic.decider to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.constructionheuristic.decider.forager to ai.timefold.solver.enterprise.core; + exports ai.timefold.solver.core.impl.constructionheuristic.placer to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.domain.variable to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.domain.variable.supply to ai.timefold.solver.enterprise.core; exports ai.timefold.solver.core.impl.domain.variable.listener.support to ai.timefold.solver.enterprise.core; diff --git a/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java b/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java deleted file mode 100644 index a3174ae542b..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java +++ /dev/null @@ -1,678 +0,0 @@ -package ai.timefold.solver.core.api.score.analysis; - -import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_ALL; -import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT; -import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_SHALLOW; -import static ai.timefold.solver.core.impl.score.director.InnerScoreDirector.getConstraintAnalysis; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; -import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; - -import org.junit.jupiter.api.Test; - -class ScoreAnalysisTest { - - @Test - void empty() { - var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(0), Collections.emptyMap()); - var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(0), Collections.emptyMap()); - - var diff = scoreAnalysis.diff(scoreAnalysis2); - assertSoftly(softly -> { - softly.assertThat(diff.score()).isEqualTo(SimpleScore.of(0)); - softly.assertThat(diff.constraintMap()).isEmpty(); - }); - - var summary = scoreAnalysis.summarize(); - assertThat(summary) - .isEqualTo(""" - Explanation of score (0): - Constraint matches: - """); - } - - @Test - void summarize() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintName3 = "constraint3"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - var constraintId3 = ConstraintRef.of(constraintName3); - - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(4), "A", "B"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(7), "C"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(8)); - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(10), "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); - var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); - var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), - getConstraintAnalysis(constraintMatchTotal, ScoreAnalysisFetchPolicy.FETCH_ALL), - constraintMatchTotal2.getConstraintRef(), - getConstraintAnalysis(constraintMatchTotal2, ScoreAnalysisFetchPolicy.FETCH_ALL), - emptyConstraintMatchTotal1.getConstraintRef(), - getConstraintAnalysis(emptyConstraintMatchTotal1, ScoreAnalysisFetchPolicy.FETCH_ALL)); - var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(67), constraintAnalysisMap); - - // Single constraint analysis - var constraintSummary = constraintAnalysisMap.get(constraintMatchTotal.getConstraintRef()).summarize(); - assertThat(constraintSummary) - .isEqualTo(""" - Explanation of score (27): - Constraint matches: - 27: constraint (constraint1) has 5 matches: - 2: justified with ([A, B, C]) - 4: justified with ([A, B]) - 6: justified with ([B, C]) - ... and 2 more matches - """); - - // Complete score analysis - var summary = scoreAnalysis.summarize(); - assertThat(scoreAnalysis.getConstraintAnalysis(constraintName1).matchCount()).isEqualTo(5); - assertThat(summary) - .isEqualTo(""" - Explanation of score (67): - Constraint matches: - 0: constraint (constraint3) has no matches. - 27: constraint (constraint1) has 5 matches: - 2: justified with ([A, B, C]) - 4: justified with ([A, B]) - 6: justified with ([B, C]) - ... and 2 more matches - 40: constraint (constraint2) has 5 matches: - 3: justified with ([B, C, D]) - 6: justified with ([B, C]) - 9: justified with ([C, D]) - ... and 2 more matches - """); - } - - @Test - void summarizeUninitializedSolution() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(0)); - var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), - getConstraintAnalysis(constraintMatchTotal, ScoreAnalysisFetchPolicy.FETCH_ALL), - constraintMatchTotal2.getConstraintRef(), - getConstraintAnalysis(constraintMatchTotal2, ScoreAnalysisFetchPolicy.FETCH_ALL)); - var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.ZERO, constraintAnalysisMap, false); - - // Single constraint analysis - var constraintSummary = constraintAnalysisMap.get(constraintMatchTotal.getConstraintRef()).summarize(); - assertThat(constraintSummary) - .isEqualTo(""" - Explanation of score (0): - Constraint matches: - 0: constraint (constraint1) has no matches. - """); - - // Complete score analysis - var summary = scoreAnalysis.summarize(); - assertThat(scoreAnalysis.getConstraintAnalysis(constraintName1).matchCount()).isZero(); - assertThat(summary) - .isEqualTo(""" - Explanation of score (0): - Constraint matches: - 0: constraint (constraint1) has no matches. - 0: constraint (constraint2) has no matches. - """); - } - - @Test - void failFastSummarize() { - var constraintName1 = "constraint1"; - var constraintId1 = ConstraintRef.of(constraintName1); - - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(2), "A", "B", "C"); - var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal, FETCH_SHALLOW)); - var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(3), constraintAnalysisMap); - - assertThatThrownBy(scoreAnalysis::summarize) - .hasMessageContaining("The constraint matches must be non-null"); - - assertThat(constraintAnalysisMap.values().stream().findFirst().get().matchCount()) - .isEqualTo(-1); - } - - @Test - void summarizeWithLimit() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(4), "A", "B"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(7), "C"); - addConstraintMatch(constraintMatchTotal, SimpleScore.of(8)); - - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); - - var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), - getConstraintAnalysis(constraintMatchTotal, ScoreAnalysisFetchPolicy.FETCH_ALL), - constraintMatchTotal2.getConstraintRef(), - getConstraintAnalysis(constraintMatchTotal2, ScoreAnalysisFetchPolicy.FETCH_ALL)); - var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(36), constraintAnalysisMap); - - // Test with limit of 2 - var summaryWith2 = scoreAnalysis.summarize(2); - assertThat(summaryWith2).contains("... and 3 more matches"); - assertThat(summaryWith2).doesNotContain("justified with ([C])"); - assertThat(summaryWith2).contains("justified with ([A, B, C])"); - assertThat(summaryWith2).contains("justified with ([A, B])"); - - // Test with limit of 10 (more than available) - var summaryWith10 = scoreAnalysis.summarize(10); - assertThat(summaryWith10).doesNotContain("... and"); - assertThat(summaryWith10).contains("justified with ([C])"); - - // Test with Integer.MAX_VALUE - var summaryWithAll = scoreAnalysis.summarize(Integer.MAX_VALUE); - assertThat(summaryWithAll).doesNotContain("..."); - assertThat(summaryWithAll).contains("justified with ([C])"); - - // Test individual constraint analysis - var constraintSummaryWith1 = constraintAnalysisMap.get(constraintMatchTotal.getConstraintRef()).summarize(1); - assertThat(constraintSummaryWith1).contains("... and 4 more matches"); - assertThat(constraintSummaryWith1).contains("justified with ([A, B, C])"); - assertThat(constraintSummaryWith1).doesNotContain("justified with ([A, B])"); - } - - @Test - void summarizeWithInvalidLimit() { - var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(0), Collections.emptyMap()); - - assertThatThrownBy(() -> scoreAnalysis.summarize(0)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("The topLimit (0) must be at least 1."); - - assertThatThrownBy(() -> scoreAnalysis.summarize(-1)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("The topLimit (-1) must be at least 1."); - } - - @Test - void diffWithConstraintMatchesWithoutMatchAnalysis() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintName3 = "constraint3"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - var constraintId3 = ConstraintRef.of(constraintName3); - - var constraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(4), "A", "B"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(8)); - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); - var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); - var constraintAnalysisMap1 = Map.of( - constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_SHALLOW), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_SHALLOW), - emptyConstraintMatchTotal1.getConstraintRef(), - getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_SHALLOW)); - var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); - - var emptyConstraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); - var constraintMatchTotal3 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(2)); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(4), "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(6), "A", "B"); - var constraintMatchTotal4 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); - var constraintAnalysisMap2 = Map.of( - emptyConstraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal2, FETCH_SHALLOW), - constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_SHALLOW), - constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_SHALLOW)); - var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); - - var diff = scoreAnalysis1.diff(scoreAnalysis2); - assertSoftly(softly -> { - softly.assertThat(diff.score()).isEqualTo(SimpleScore.of(8)); - softly.assertThat(diff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal2.getConstraintRef(), - constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 not present. - var constraintAnalysis1 = diff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); - softly.assertThat(constraintAnalysis1.matches()).isNull(); - }); - // Matches for constraint2 still not present. - var constraintAnalysis2 = diff.getConstraintAnalysis(constraintName2); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); - softly.assertThat(constraintAnalysis2.matches()).isNull(); - }); - // Matches for constraint3 not present. - var constraintAnalysis3 = diff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); - softly.assertThat(constraintAnalysis3.matches()).isNull(); - }); - - var reverseDiff = scoreAnalysis2.diff(scoreAnalysis1); - assertSoftly(softly -> { - softly.assertThat(reverseDiff.score()).isEqualTo(SimpleScore.of(-8)); - softly.assertThat(reverseDiff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal3.getConstraintRef(), - constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 not present. - var reverseConstraintAnalysis1 = reverseDiff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); - softly.assertThat(reverseConstraintAnalysis1.matches()).isNull(); - }); - // Matches for constraint2 still not present. - var reverseConstraintAnalysis2 = reverseDiff.getConstraintAnalysis(constraintName2); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); - softly.assertThat(reverseConstraintAnalysis2.matches()).isNull(); - }); - // Matches for constraint3 not present in reverse. - var reverseConstraintAnalysis3 = reverseDiff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); - softly.assertThat(reverseConstraintAnalysis3.matches()).isNull(); - }); - } - - @Test - void diffWithConstraintMatchesWithMatchCountOnly() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintName3 = "constraint3"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - var constraintId3 = ConstraintRef.of(constraintName3); - - var constraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(4), "A", "B"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(8)); - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); - var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); - var constraintAnalysisMap1 = Map.of( - constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_MATCH_COUNT), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_MATCH_COUNT), - emptyConstraintMatchTotal1.getConstraintRef(), - getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_MATCH_COUNT)); - var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); - - var emptyConstraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); - var constraintMatchTotal3 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(2)); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(4), "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(6), "A", "B"); - var constraintMatchTotal4 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); - var constraintAnalysisMap2 = Map.of( - emptyConstraintMatchTotal2.getConstraintRef(), - getConstraintAnalysis(emptyConstraintMatchTotal2, FETCH_MATCH_COUNT), - constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_MATCH_COUNT), - constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_MATCH_COUNT)); - var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); - - var diff = scoreAnalysis1.diff(scoreAnalysis2); - assertSoftly(softly -> { - softly.assertThat(diff.score()).isEqualTo(SimpleScore.of(8)); - softly.assertThat(diff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal2.getConstraintRef(), - constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 not present. - var constraintAnalysis1 = diff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); - softly.assertThat(constraintAnalysis1.matches()).isNull(); - softly.assertThat(constraintAnalysis1.matchCount()).isGreaterThan(0); - }); - // Matches for constraint2 still not present. - var constraintAnalysis2 = diff.getConstraintAnalysis(constraintName2); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); - softly.assertThat(constraintAnalysis2.matches()).isNull(); - softly.assertThat(constraintAnalysis2.matchCount()).isGreaterThan(0); - }); - // Matches for constraint3 not present. - var constraintAnalysis3 = diff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); - softly.assertThat(constraintAnalysis3.matches()).isNull(); - softly.assertThat(constraintAnalysis3.matchCount()).isLessThan(0); - }); - - var reverseDiff = scoreAnalysis2.diff(scoreAnalysis1); - assertSoftly(softly -> { - softly.assertThat(reverseDiff.score()).isEqualTo(SimpleScore.of(-8)); - softly.assertThat(reverseDiff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal3.getConstraintRef(), - constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 not present. - var reverseConstraintAnalysis1 = reverseDiff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); - softly.assertThat(reverseConstraintAnalysis1.matches()).isNull(); - softly.assertThat(reverseConstraintAnalysis1.matchCount()).isLessThan(0); - }); - // Matches for constraint2 still not present. - var reverseConstraintAnalysis2 = reverseDiff.getConstraintAnalysis(constraintName2); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); - softly.assertThat(reverseConstraintAnalysis2.matches()).isNull(); - softly.assertThat(reverseConstraintAnalysis2.matchCount()).isLessThan(0); - }); - // Matches for constraint3 not present in reverse. - var reverseConstraintAnalysis3 = reverseDiff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); - softly.assertThat(reverseConstraintAnalysis3.matches()).isNull(); - softly.assertThat(reverseConstraintAnalysis3.matchCount()).isGreaterThan(0); - }); - } - - @Test - void diffWithConstraintMatchesAndMatchAnalysis() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintName3 = "constraint3"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - var constraintId3 = ConstraintRef.of(constraintName3); - - var constraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(4), "A", "B"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(8)); - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); - var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); - var constraintAnalysisMap1 = Map.of( - constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_ALL), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_ALL), - emptyConstraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_ALL)); - var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); - - var emptyConstraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); - var constraintMatchTotal3 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(2)); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(4), "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(6), "A", "B"); - var constraintMatchTotal4 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); - var constraintAnalysisMap2 = Map.of( - emptyConstraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal2, FETCH_ALL), - constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_ALL), - constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_ALL)); - var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); - - var diff = scoreAnalysis1.diff(scoreAnalysis2); - assertSoftly(softly -> { - softly.assertThat(diff.score()).isEqualTo(SimpleScore.of(8)); - softly.assertThat(diff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal2.getConstraintRef(), - constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 present. - var constraintAnalysis1 = diff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); - var matchAnalyses = constraintAnalysis1.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(constraintAnalysis1.constraintRef(), 2, "A", "B", "C"), - matchAnalysisOf(constraintAnalysis1.constraintRef(), 4, "A", "B"), - matchAnalysisOf(constraintAnalysis1.constraintRef(), 6, "B", "C"), - matchAnalysisOf(constraintAnalysis1.constraintRef(), 8)); - }); - // Matches for constraint2 present in both. - var constraintAnalysis2 = diff.getConstraintAnalysis(constraintName2); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); - var matchAnalyses = constraintAnalysis2.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(constraintAnalysis2.constraintRef(), 3, "B", "C", "D"), - matchAnalysisOf(constraintAnalysis2.constraintRef(), 2, "B", "C"), - matchAnalysisOf(constraintAnalysis2.constraintRef(), 9, "C", "D"), - matchAnalysisOf(constraintAnalysis2.constraintRef(), 12), - matchAnalysisOf(constraintAnalysis2.constraintRef(), -2, "A", "B", "C"), - matchAnalysisOf(constraintAnalysis2.constraintRef(), -6, "A", "B")); - }); - // Matches for constraint3 not present. - var constraintAnalysis3 = diff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); - var matchAnalyses = constraintAnalysis3.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(constraintAnalysis3.constraintRef(), -3, "B", "C", "D"), - matchAnalysisOf(constraintAnalysis3.constraintRef(), -6, "B", "C"), - matchAnalysisOf(constraintAnalysis3.constraintRef(), -9, "C", "D"), - matchAnalysisOf(constraintAnalysis3.constraintRef(), -12)); - }); - - var reverseDiff = scoreAnalysis2.diff(scoreAnalysis1); - assertSoftly(softly -> { - softly.assertThat(reverseDiff.score()).isEqualTo(SimpleScore.of(-8)); - softly.assertThat(reverseDiff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal3.getConstraintRef(), - constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 not present. - var reverseConstraintAnalysis1 = reverseDiff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); - var matchAnalyses = reverseConstraintAnalysis1.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -2, "A", "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -4, "A", "B"), - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -6, "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -8)); - }); - // Matches for constraint2 present in both. - var reverseConstraintAnalysis2 = reverseDiff.getConstraintAnalysis(constraintName2); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); - var matchAnalyses = reverseConstraintAnalysis2.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), -3, "B", "C", "D"), - matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), -2, "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), -9, "C", "D"), - matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), -12), - matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), 2, "A", "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), 6, "A", "B")); - }); - // Matches for constraint3 present in reverse. - var reverseConstraintAnalysis3 = reverseDiff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); - var matchAnalyses = reverseConstraintAnalysis3.matches(); - softly.assertThat(matchAnalyses) - .containsOnly(matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 3, "B", "C", "D"), - matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 6, "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 9, "C", "D"), - matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 12)); - }); - } - - @Test - void diffWithConstraintMatchesAndMatchAnalysisWithSomeIdenticalMatches() { - var constraintName1 = "constraint1"; - var constraintName2 = "constraint2"; - var constraintName3 = "constraint3"; - var constraintId1 = ConstraintRef.of(constraintName1); - var constraintId2 = ConstraintRef.of(constraintName2); - var constraintId3 = ConstraintRef.of(constraintName3); - - var constraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(4), "A", "B"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal1, SimpleScore.of(8)); - var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); - var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); - var constraintAnalysisMap1 = Map.of( - constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_ALL), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_ALL), - emptyConstraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_ALL)); - var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); - - var constraintMatchTotal3 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(2)); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(2), "A", "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(4), "B", "C"); - addConstraintMatch(constraintMatchTotal3, SimpleScore.of(6), "A", "B"); - var constraintMatchTotal4 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(3)); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(3), "B", "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(6), "B", "C"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); - addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); - var constraintAnalysisMap2 = Map.of( - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_ALL), - constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_ALL), - constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_ALL)); - var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); - - var diff = scoreAnalysis1.diff(scoreAnalysis2); - assertSoftly(softly -> { - softly.assertThat(diff.score()).isEqualTo(SimpleScore.of(8)); - softly.assertThat(diff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 present. - var constraintAnalysis1 = diff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(8)); - var matchAnalyses = constraintAnalysis1.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(constraintAnalysis1.constraintRef(), -2, "A", "B"), - matchAnalysisOf(constraintAnalysis1.constraintRef(), 2, "B", "C"), - matchAnalysisOf(constraintAnalysis1.constraintRef(), 8)); - }); - // Identical matches for constraint2 present in both - var constraintAnalysis2 = diff.getConstraintAnalysis(constraintName2); - assertThat(constraintAnalysis2).isNull(); - - // Matches for constraint3 not present. - var constraintAnalysis3 = diff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); - var matchAnalyses = constraintAnalysis3.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(constraintAnalysis3.constraintRef(), -3, "B", "C", "D"), - matchAnalysisOf(constraintAnalysis3.constraintRef(), -6, "B", "C"), - matchAnalysisOf(constraintAnalysis3.constraintRef(), -9, "C", "D"), - matchAnalysisOf(constraintAnalysis3.constraintRef(), -12)); - }); - - var reverseDiff = scoreAnalysis2.diff(scoreAnalysis1); - assertSoftly(softly -> { - softly.assertThat(reverseDiff.score()).isEqualTo(SimpleScore.of(-8)); - softly.assertThat(reverseDiff.constraintMap()) - .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal4.getConstraintRef()); - }); - // Matches for constraint1 not present. - var reverseConstraintAnalysis1 = reverseDiff.getConstraintAnalysis(constraintName1); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-8)); - var matchAnalyses = reverseConstraintAnalysis1.matches(); - softly.assertThat(matchAnalyses) - .containsOnly( - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), 2, "A", "B"), - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -2, "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -8)); - }); - // Identical matches for constraint2 present in both - var reverseConstraintAnalysis2 = reverseDiff.getConstraintAnalysis(constraintName2); - assertThat(reverseConstraintAnalysis2).isNull(); - // Matches for constraint3 present in reverse. - var reverseConstraintAnalysis3 = reverseDiff.getConstraintAnalysis(constraintName3); - assertSoftly(softly -> { - softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); - var matchAnalyses = reverseConstraintAnalysis3.matches(); - softly.assertThat(matchAnalyses) - .containsOnly(matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 3, "B", "C", "D"), - matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 6, "B", "C"), - matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 9, "C", "D"), - matchAnalysisOf(reverseConstraintAnalysis3.constraintRef(), 12)); - }); - } - - private void addConstraintMatch(DefaultConstraintMatchTotal constraintMatchTotal, SimpleScore impact, - Object... indictments) { - constraintMatchTotal.addConstraintMatch(DefaultConstraintJustification.of(impact, indictments), - Arrays.asList(indictments), impact); - } - - private static MatchAnalysis matchAnalysisOf(ConstraintRef constraintRef, int score, Object... facts) { - var simpleScore = SimpleScore.of(score); - return new MatchAnalysis<>(constraintRef, simpleScore, DefaultConstraintJustification.of(simpleScore, facts)); - } - -} diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java index 09636d1fe38..43bca61bdc7 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java @@ -15,33 +15,21 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.impl.solver.DefaultSolutionManager; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.testdomain.TestdataConstraintProvider; +import ai.timefold.solver.core.testdomain.TestdataEasyScoreCalculator; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; -import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListCMAIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListEntity; -import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListSolution; -import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListValue; import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListEntityWithShadowHistory; import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListSolutionWithShadowHistory; import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListValueWithShadowHistory; -import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListWithShadowHistoryIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution; -import ai.timefold.solver.core.testdomain.multivar.TestdataMultivarIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.multivar.TestdataOtherValue; +import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListWithShadowHistoryConstraintProvider; +import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedConstraintProviderClass; import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedEntity; -import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedIncrementalScoreCalculator; import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedSolution; import ai.timefold.solver.core.testdomain.shadow.concurrent.TestdataConcurrentConstraintProvider; import ai.timefold.solver.core.testdomain.shadow.concurrent.TestdataConcurrentEntity; import ai.timefold.solver.core.testdomain.shadow.concurrent.TestdataConcurrentSolution; import ai.timefold.solver.core.testdomain.shadow.concurrent.TestdataConcurrentValue; -import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEntity; -import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedSolution; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; @@ -50,79 +38,60 @@ public class SolutionManagerTest { - public static final SolverFactory SOLVER_FACTORY = - SolverFactory.createFromXmlResource("ai/timefold/solver/core/api/solver/testdataShadowedSolverConfig.xml"); - public static final SolverFactory SOLVER_FACTORY_OVERCONSTRAINED = - SolverFactory.createFromXmlResource("ai/timefold/solver/core/api/solver/testdataOverconstrainedSolverConfig.xml"); public static final SolverFactory SOLVER_FACTORY_SHADOWED = SolverFactory.create( new SolverConfig() .withSolutionClass(TestdataShadowedSolution.class) .withEntityClasses(TestdataShadowedEntity.class) .withScoreDirectorFactory( - new ScoreDirectorFactoryConfig().withIncrementalScoreCalculatorClass( - TestdataShadowedIncrementalScoreCalculator.class))); + new ScoreDirectorFactoryConfig() + .withConstraintProviderClass(TestdataShadowedConstraintProviderClass.class))); public static final SolverFactory SOLVER_FACTORY_DECLARATIVE_SHADOW = SolverFactory.create( new SolverConfig() .withSolutionClass(TestdataConcurrentSolution.class) .withEntityClasses(TestdataConcurrentEntity.class, TestdataConcurrentValue.class) .withConstraintProviderClass(TestdataConcurrentConstraintProvider.class)); - public static final SolverFactory SOLVER_FACTORY_UNASSIGNED = SolverFactory.create( - new SolverConfig() - .withSolutionClass(TestdataAllowsUnassignedSolution.class) - .withEntityClasses(TestdataAllowsUnassignedEntity.class) - .withScoreDirectorFactory( - new ScoreDirectorFactoryConfig().withIncrementalScoreCalculatorClass( - TestdataAllowsUnassignedIncrementalScoreCalculator.class))); - public static final SolverFactory SOLVER_FACTORY_MULTIVAR = SolverFactory.create( - new SolverConfig() - .withSolutionClass(TestdataMultiVarSolution.class) - .withEntityClasses(TestdataMultiVarEntity.class) - .withScoreDirectorFactory( - new ScoreDirectorFactoryConfig().withIncrementalScoreCalculatorClass( - TestdataMultivarIncrementalScoreCalculator.class))); public static final SolverFactory SOLVER_FACTORY_LIST = SolverFactory.create( new SolverConfig() .withSolutionClass(TestdataListSolutionWithShadowHistory.class) .withEntityClasses(TestdataListEntityWithShadowHistory.class, TestdataListValueWithShadowHistory.class) .withScoreDirectorFactory( - new ScoreDirectorFactoryConfig().withIncrementalScoreCalculatorClass( - TestdataListWithShadowHistoryIncrementalScoreCalculator.class))); - public static final SolverFactory SOLVER_FACTORY_LIST_PINNED = SolverFactory.create( - new SolverConfig() - .withSolutionClass(TestdataPinnedWithIndexListSolution.class) - .withEntityClasses(TestdataPinnedWithIndexListEntity.class, TestdataPinnedWithIndexListValue.class) - .withScoreDirectorFactory( - new ScoreDirectorFactoryConfig().withIncrementalScoreCalculatorClass( - TestdataPinnedWithIndexListCMAIncrementalScoreCalculator.class))); + new ScoreDirectorFactoryConfig() + .withConstraintProviderClass(TestdataListWithShadowHistoryConstraintProvider.class))); public static final SolverFactory SOLVER_FACTORY_WITH_CS = SolverFactory.create( new SolverConfig() .withSolutionClass(TestdataSolution.class) .withEntityClasses(TestdataEntity.class) .withConstraintProviderClass(TestdataConstraintProvider.class)); + public static final SolverFactory SOLVER_FACTORY_EASY = SolverFactory.create( + new SolverConfig() + .withSolutionClass(TestdataSolution.class) + .withEntityClasses(TestdataEntity.class) + .withScoreDirectorFactory( + new ScoreDirectorFactoryConfig().withEasyScoreCalculatorClass(TestdataEasyScoreCalculator.class))); @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void updateEverything(SolutionManagerSource SolutionManagerSource) { + void updateEverything(SolutionManagerSource solutionManagerSource) { var solution = TestdataShadowedSolution.generateSolution(); assertSoftly(softly -> { softly.assertThat(solution.getScore()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getFirstShadow()).isNull(); + softly.assertThat(solution.getEntityList().getFirst().getFirstShadow()).isNull(); }); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); + var solutionManager = solutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); assertThat(solutionManager).isNotNull(); solutionManager.update(solution); assertSoftly(softly -> { softly.assertThat(solution.getScore()).isNotNull(); - softly.assertThat(solution.getEntityList().get(0).getFirstShadow()).isNotNull(); + softly.assertThat(solution.getEntityList().getFirst().getFirstShadow()).isNotNull(); }); } @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void updateEverythingList(SolutionManagerSource SolutionManagerSource) { + void updateEverythingList(SolutionManagerSource solutionManagerSource) { var a0 = new TestdataListValueWithShadowHistory("a0"); var a1 = new TestdataListValueWithShadowHistory("a1"); var a = new TestdataListEntityWithShadowHistory("a", a0, a1); @@ -147,7 +116,7 @@ void updateEverythingList(SolutionManagerSource SolutionManagerSource) { assertShadowedListValueAllNull(softly, c0); }); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST); + var solutionManager = solutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST); assertThat(solutionManager).isNotNull(); solutionManager.update(solution); @@ -180,47 +149,47 @@ private void assertShadowedListValue(SoftAssertions softly, TestdataListValueWit @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void updateOnlyShadowVariables(SolutionManagerSource SolutionManagerSource) { + void updateOnlyShadowVariables(SolutionManagerSource solutionManagerSource) { var solution = TestdataShadowedSolution.generateSolution(); assertSoftly(softly -> { softly.assertThat(solution.getScore()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getFirstShadow()).isNull(); + softly.assertThat(solution.getEntityList().getFirst().getFirstShadow()).isNull(); }); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); + var solutionManager = solutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); assertThat(solutionManager).isNotNull(); solutionManager.update(solution, SolutionUpdatePolicy.UPDATE_SHADOW_VARIABLES_ONLY); assertSoftly(softly -> { softly.assertThat(solution.getScore()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getFirstShadow()).isNotNull(); + softly.assertThat(solution.getEntityList().getFirst().getFirstShadow()).isNotNull(); }); } @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void updateOnlyScore(SolutionManagerSource SolutionManagerSource) { + void updateOnlyScore(SolutionManagerSource solutionManagerSource) { var solution = TestdataShadowedSolution.generateSolution(); assertSoftly(softly -> { softly.assertThat(solution.getScore()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getFirstShadow()).isNull(); + softly.assertThat(solution.getEntityList().getFirst().getFirstShadow()).isNull(); }); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); + var solutionManager = solutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); assertThat(solutionManager).isNotNull(); solutionManager.update(solution, SolutionUpdatePolicy.UPDATE_SCORE_ONLY); assertSoftly(softly -> { softly.assertThat(solution.getScore()).isNotNull(); - softly.assertThat(solution.getEntityList().get(0).getFirstShadow()).isNull(); + softly.assertThat(solution.getEntityList().getFirst().getFirstShadow()).isNull(); }); } @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void updateOnlyScoreDeclarativeShadows(SolutionManagerSource SolutionManagerSource) { + void updateOnlyScoreDeclarativeShadows(SolutionManagerSource solutionManagerSource) { var solution = new TestdataConcurrentSolution(); var e1 = new TestdataConcurrentEntity("e1"); var e2 = new TestdataConcurrentEntity("e2"); @@ -270,7 +239,7 @@ void updateOnlyScoreDeclarativeShadows(SolutionManagerSource SolutionManagerSour solution.setEntities(entities); solution.setValues(values); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_DECLARATIVE_SHADOW); + var solutionManager = solutionManagerSource.createSolutionManager(SOLVER_FACTORY_DECLARATIVE_SHADOW); var score = solutionManager.update(solution, SolutionUpdatePolicy.UPDATE_SCORE_ONLY); assertThat(score).isEqualTo(HardSoftScore.ofHard(-4)); assertThat(solution.getScore()).isEqualTo(HardSoftScore.ofHard(-4)); @@ -324,7 +293,7 @@ void updateShadowVariableFailsIfReferencedEntitiesAreNotGiven() { @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void updateOnlyScoreFailsIfListVariableInconsistent(SolutionManagerSource SolutionManagerSource) { + void updateOnlyScoreFailsIfListVariableInconsistent(SolutionManagerSource solutionManagerSource) { var solution = new TestdataConcurrentSolution(); var e1 = new TestdataConcurrentEntity("e1"); var e2 = new TestdataConcurrentEntity("e2"); @@ -374,11 +343,9 @@ void updateOnlyScoreFailsIfListVariableInconsistent(SolutionManagerSource Soluti solution.setEntities(entities); solution.setValues(values); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_DECLARATIVE_SHADOW); + var solutionManager = solutionManagerSource.createSolutionManager(SOLVER_FACTORY_DECLARATIVE_SHADOW); assertThat(solutionManager).isNotNull(); - assertThatCode(() -> { - solutionManager.update(solution, SolutionUpdatePolicy.UPDATE_SCORE_ONLY); - }).hasMessageContainingAll( + assertThatCode(() -> solutionManager.update(solution, SolutionUpdatePolicy.UPDATE_SCORE_ONLY)).hasMessageContainingAll( "The entity (e1)" + " has a list variable (values)" + " and one of its elements (e1 -> a1 -> b1)" + @@ -386,957 +353,12 @@ void updateOnlyScoreFailsIfListVariableInconsistent(SolutionManagerSource Soluti " has an oldInverseEntity (e2) which is not that entity."); } - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void explain(SolutionManagerSource SolutionManagerSource) { - var solution = TestdataShadowedSolution.generateSolution(); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY); - assertThat(solutionManager).isNotNull(); - - var scoreExplanation = solutionManager.explain(solution); - assertThat(scoreExplanation).isNotNull(); - assertSoftly(softly -> { - softly.assertThat(scoreExplanation.getScore()).isNotNull(); - softly.assertThat(scoreExplanation.getSummary()).isNotBlank(); - softly.assertThat(scoreExplanation.getConstraintMatchTotalMap()) - .containsOnlyKeys("testConstraint"); - softly.assertThat(scoreExplanation.getIndictmentMap()) - .containsOnlyKeys(solution.getEntityList().toArray()); - - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void analyze(SolutionManagerSource SolutionManagerSource) { - var solution = TestdataShadowedSolution.generateSolution(); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY); - assertThat(solutionManager).isNotNull(); - - var scoreAnalysis = solutionManager.analyze(solution); - assertThat(scoreAnalysis).isNotNull(); - assertSoftly(softly -> { - softly.assertThat(scoreAnalysis.score()).isNotNull(); - softly.assertThat(scoreAnalysis.constraintMap()).isNotEmpty(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void analyzeNonNullableWithNullValue(SolutionManagerSource SolutionManagerSource) { - var solution = TestdataShadowedSolution.generateSolution(); - solution.getEntityList().get(0).setValue(null); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY); - assertThat(solutionManager).isNotNull(); - - assertThat(solutionManager.analyze(solution)).isNotNull(); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void analyzeNullableWithNullValue(SolutionManagerSource SolutionManagerSource) { - var solution = TestdataAllowsUnassignedSolution.generateSolution(); - solution.getEntityList().get(0).setValue(null); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_OVERCONSTRAINED); - assertThat(solutionManager).isNotNull(); - - var scoreAnalysis = solutionManager.analyze(solution); - assertThat(scoreAnalysis).isNotNull(); - assertSoftly(softly -> { - softly.assertThat(scoreAnalysis.score()).isNotNull(); - softly.assertThat(scoreAnalysis.constraintMap()).isNotEmpty(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void analyzeWithUninitializedSolution(SolutionManagerSource SolutionManagerSource) { - var uninitializedSolution = TestdataShadowedSolution.generateSolution(3, 3); - uninitializedSolution.getEntityList().forEach(e -> e.setValue(null)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY); - assertThat(solutionManager).isNotNull(); - - var scoreAnalysis = solutionManager.analyze(uninitializedSolution); - assertThat(scoreAnalysis).isNotNull(); - assertSoftly(softly -> { - softly.assertThat(scoreAnalysis.score()).isNotNull(); - softly.assertThat(scoreAnalysis.constraintMap()).isNotEmpty(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignment(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var solution = TestdataShadowedSolution.generateSolution(valueSize, 3); - var uninitializedEntity = solution.getEntityList().get(2); - var unassignedValue = uninitializedEntity.getValue(); - uninitializedEntity.setValue(null); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); - assertThat(solutionManager).isNotNull(); - var recommendationList = - solutionManager.recommendAssignment(solution, uninitializedEntity, TestdataShadowedEntity::getValue); - - // Three values means there need to be three recommendations. - assertThat(recommendationList).hasSize(valueSize); - /* - * The calculator counts how many entities have the same value as another entity. - * Therefore the recommendation to assign value #2 needs to come first, - * as it means each entity only has each value once. - */ - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - softly.assertThat(firstRecommendation.proposition()).isEqualTo(unassignedValue); - softly.assertThat(firstRecommendation.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-1)); - }); - // The other two recommendations need to come in order of the placer; so value #0, then value #1. - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - softly.assertThat(secondRecommendation.proposition()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(secondRecommendation.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-3)); - }); - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - softly.assertThat(thirdRecommendation.proposition()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-3)); - }); - // Ensure the original solution is in its original state. - assertSoftly(softly -> { - softly.assertThat(uninitializedEntity.getValue()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getValue()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(solution.getEntityList().get(1).getValue()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(solution.getScore()).isNull(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentAlreadyAssigned(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var solution = TestdataShadowedSolution.generateSolution(valueSize, 3); - var evaluatedEntity = solution.getEntityList().get(2); - var value = evaluatedEntity.getValue(); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); - assertThat(solutionManager).isNotNull(); - var recommendationList = - solutionManager.recommendAssignment(solution, evaluatedEntity, TestdataShadowedEntity::getValue); - - // Three values means there need to be three recommendations. - assertThat(recommendationList).hasSize(valueSize); - /* - * The calculator counts how many entities have the same value as another entity. - * Therefore the recommendation to assign value #2 needs to come first, - * as it means the solution is practically unchanged. - */ - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - softly.assertThat(firstRecommendation.proposition()).isEqualTo(value); - softly.assertThat(firstRecommendation.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(0)); - }); - // The other two recommendations need to come in order of the placer; so value #0, then value #1. - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - softly.assertThat(secondRecommendation.proposition()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(secondRecommendation.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-2)); - }); - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - softly.assertThat(thirdRecommendation.proposition()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-2)); - }); - // Ensure the original solution is in its original state. - assertSoftly(softly -> { - softly.assertThat(evaluatedEntity.getValue()).isEqualTo(value); - softly.assertThat(solution.getEntityList().get(0).getValue()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(solution.getEntityList().get(1).getValue()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(solution.getScore()).isNull(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentUninitializedSolution(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var uninitializedSolution = TestdataShadowedSolution.generateSolution(valueSize, 3); - uninitializedSolution.getEntityList().forEach(e -> e.setValue(null)); - var uninitializedEntity = uninitializedSolution.getEntityList().get(2); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_SHADOWED); - assertThat(solutionManager).isNotNull(); - assertThatThrownBy(() -> solutionManager.recommendAssignment(uninitializedSolution, uninitializedEntity, - TestdataShadowedEntity::getValue)) - .hasMessageContaining("Solution (Generated Solution 0) has (3) uninitialized elements."); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentWithUnassigned(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var solution = TestdataAllowsUnassignedSolution.generateSolution(valueSize, 3); - var uninitializedEntity = solution.getEntityList().get(2); - uninitializedEntity.setValue(null); - - // At this point, entity 0 and entity 2 are unassigned. - // Entity 1 is assigned to value #1. - // But only entity2 should be processed for recommendations. - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_UNASSIGNED); - assertThat(solutionManager).isNotNull(); - var recommendationList = - solutionManager.recommendAssignment(solution, uninitializedEntity, TestdataAllowsUnassignedEntity::getValue); - - // Three values means there need to be four recommendations, one extra for unassigned. - assertThat(recommendationList).hasSize(valueSize + 1); - /* - * The calculator penalizes how many entities have the same value as another entity. - * Therefore the recommendation to assign value 0 and value 2 need to come first and in the order of the placer, - * as it means two entities no longer share a value, improving the score. - */ - var recommendation1 = recommendationList.get(0); - assertSoftly(softly -> { - softly.assertThat(recommendation1.proposition()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(recommendation1.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(2)); // Two entities no longer share null value. - }); - var recommendation2 = recommendationList.get(1); - assertSoftly(softly -> { - softly.assertThat(recommendation2.proposition()).isEqualTo(solution.getValueList().get(2)); - softly.assertThat(recommendation2.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(2)); - }); - // The other two recommendations need to come in order of the placer; so null, then value #1. - var recommendation3 = recommendationList.get(2); - assertSoftly(softly -> { - softly.assertThat(recommendation3.proposition()).isEqualTo(null); - softly.assertThat(recommendation3.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.ZERO); - }); - var recommendation4 = recommendationList.get(3); - assertSoftly(softly -> { - softly.assertThat(recommendation4.proposition()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(recommendation4.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.ZERO); - }); - // Ensure the original solution is in its original state. - assertSoftly(softly -> { - softly.assertThat(uninitializedEntity.getValue()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getValue()).isNull(); - softly.assertThat(solution.getEntityList().get(1).getValue()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(solution.getEntityList().get(2).getValue()).isNull(); - softly.assertThat(solution.getScore()).isNull(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentWithUnassignedMatchCount(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var solution = TestdataAllowsUnassignedSolution.generateSolution(valueSize, 3); - var uninitializedEntity = solution.getEntityList().get(2); - uninitializedEntity.setValue(null); - - // At this point, entity 0 and entity 2 are unassigned. - // Entity 1 is assigned to value #1. - // But only entity2 should be processed for recommendations. - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_UNASSIGNED); - assertThat(solutionManager).isNotNull(); - var recommendationList = - solutionManager.recommendAssignment(solution, uninitializedEntity, TestdataAllowsUnassignedEntity::getValue, - ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT); - - // Three values means there need to be four recommendations, one extra for unassigned. - assertThat(recommendationList).hasSize(valueSize + 1); - /* - * The calculator penalizes how many entities have the same value as another entity. - * Therefore the recommendation to assign value 0 and value 2 need to come first and in the order of the placer, - * as it means two entities no longer share a value, improving the score. - */ - var recommendation1 = recommendationList.get(0); - assertSoftly(softly -> { - softly.assertThat(recommendation1.proposition()).isEqualTo(solution.getValueList().get(0)); - // Two entities no longer share null value; two less matches. - softly.assertThat(recommendation1.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(2)); - softly.assertThat(recommendation1.scoreAnalysisDiff().constraintMap()) - .extractingFromEntries(e -> e.getValue().matchCount()) - .first() - .isEqualTo(-2); - }); - var recommendation2 = recommendationList.get(1); - assertSoftly(softly -> { - softly.assertThat(recommendation2.proposition()).isEqualTo(solution.getValueList().get(2)); - softly.assertThat(recommendation2.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(2)); - }); - // The other two recommendations need to come in order of the placer; so null, then value #1. - var recommendation3 = recommendationList.get(2); - assertSoftly(softly -> { - softly.assertThat(recommendation3.proposition()).isEqualTo(null); - softly.assertThat(recommendation3.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.ZERO); - }); - var recommendation4 = recommendationList.get(3); - assertSoftly(softly -> { - softly.assertThat(recommendation4.proposition()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(recommendation4.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.ZERO); - }); - // Ensure the original solution is in its original state. - assertSoftly(softly -> { - softly.assertThat(uninitializedEntity.getValue()).isNull(); - softly.assertThat(solution.getEntityList().get(0).getValue()).isNull(); - softly.assertThat(solution.getEntityList().get(1).getValue()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(solution.getEntityList().get(2).getValue()).isNull(); - softly.assertThat(solution.getScore()).isNull(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentWithUnassignedFetchAllVsFetchMatchCount(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var solution = TestdataAllowsUnassignedSolution.generateSolution(valueSize, 3); - var uninitializedEntity = solution.getEntityList().get(2); - uninitializedEntity.setValue(null); - - // At this point, entity 0 and entity 2 are unassigned. - // Entity 1 is assigned to value #1. - // But only entity2 should be processed for recommendations. - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_UNASSIGNED); - assertThat(solutionManager).isNotNull(); - var matchCountRecommendationList = - solutionManager.recommendAssignment(solution, uninitializedEntity, TestdataAllowsUnassignedEntity::getValue, - ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT); - - var justificationsRecommendationList = - solutionManager.recommendAssignment(solution, uninitializedEntity, TestdataAllowsUnassignedEntity::getValue, - ScoreAnalysisFetchPolicy.FETCH_ALL); - - // Three values means there need to be four recommendations, one extra for unassigned. - assertThat(matchCountRecommendationList).hasSize(valueSize + 1); - assertThat(justificationsRecommendationList).hasSize(valueSize + 1); - /* - * The calculator penalizes how many entities have the same value as another entity. - * Therefore the recommendation to assign value 0 and value 2 need to come first and in the order of the placer, - * as it means two entities no longer share a value, improving the score. - */ - var matchCountRecommendation1 = matchCountRecommendationList.get(0); - var justificationsRecommendation1 = justificationsRecommendationList.get(0); - - assertSoftly(softly -> { - softly.assertThat(matchCountRecommendation1.proposition()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(justificationsRecommendation1.proposition()).isEqualTo(solution.getValueList().get(0)); - - softly.assertThat(matchCountRecommendation1.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(2)); // Two entities no longer share null value. - softly.assertThat(justificationsRecommendation1.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(2)); // Two entities no longer share null value. - - // The matchCount is expected to be the same in the case of both FETCH_ALL and FETCH_MATCH_COUNT - int matchCountForFetchMatchCount = matchCountRecommendation1.scoreAnalysisDiff() - .constraintMap().values().iterator().next().matchCount(); - int matchCountForFetchAll = justificationsRecommendation1.scoreAnalysisDiff() - .constraintMap().values().iterator().next().matchCount(); - softly.assertThat(matchCountForFetchMatchCount).isEqualTo(matchCountForFetchAll); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentWithAllAssigned(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var solution = TestdataAllowsUnassignedSolution.generateSolution(valueSize, 3); - var evaluatedEntity = solution.getEntityList().get(2); - - // At this point, entity 0 is unassigned. - // Entity 1 is assigned to value #1. - // But only entity2 should be processed for recommendations. - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_UNASSIGNED); - assertThat(solutionManager).isNotNull(); - var recommendationList = - solutionManager.recommendAssignment(solution, evaluatedEntity, TestdataAllowsUnassignedEntity::getValue); - - // Three values means there need to be four recommendations, one extra for unassigned. - assertThat(recommendationList).hasSize(valueSize + 1); - /* - * The calculator penalizes how many entities have the same value as another entity. - * Therefore, the recommendation to assign value 0 needs to come first and in the order of the placer, - * as it means two entities share the value, not changing the score. - */ - var recommendation1 = recommendationList.get(0); - assertSoftly(softly -> { - softly.assertThat(recommendation1.proposition()).isEqualTo(solution.getValueList().get(0)); - softly.assertThat(recommendation1.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(0)); // Two entities share a value. - }); - var recommendation2 = recommendationList.get(1); - assertSoftly(softly -> { - softly.assertThat(recommendation2.proposition()).isEqualTo(solution.getValueList().get(2)); - softly.assertThat(recommendation2.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(0)); - }); - // The other two recommendations need to come in order of the placer; so null, then value #1. - var recommendation3 = recommendationList.get(2); - assertSoftly(softly -> { - softly.assertThat(recommendation3.proposition()).isEqualTo(null); - softly.assertThat(recommendation3.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-2)); // Two entities no longer share a value. - }); - var recommendation4 = recommendationList.get(3); - assertSoftly(softly -> { - softly.assertThat(recommendation4.proposition()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(recommendation4.scoreAnalysisDiff() - .score()).isEqualTo(SimpleScore.of(-2)); - }); - // Ensure the original solution is in its original state. - assertSoftly(softly -> { - softly.assertThat(solution.getEntityList().get(0).getValue()).isNull(); - softly.assertThat(solution.getEntityList().get(1).getValue()).isEqualTo(solution.getValueList().get(1)); - softly.assertThat(solution.getEntityList().get(2).getValue()).isEqualTo(solution.getValueList().get(2)); - softly.assertThat(solution.getScore()).isNull(); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentUninitializedSolutionWithUnassigned(SolutionManagerSource SolutionManagerSource) { - int valueSize = 3; - var uninitializedSolution = TestdataAllowsUnassignedSolution.generateSolution(valueSize, 3); - uninitializedSolution.getEntityList().forEach(e -> e.setValue(null)); - var uninitializedEntity = uninitializedSolution.getEntityList().get(2); - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_UNASSIGNED); - assertThat(solutionManager).isNotNull(); - var recommendationList = solutionManager.recommendAssignment(uninitializedSolution, uninitializedEntity, - TestdataAllowsUnassignedEntity::getValue); - - // Three values means there need to be four recommendations, one extra for unassigned. - assertThat(recommendationList).hasSize(valueSize + 1); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentMultivar(SolutionManagerSource SolutionManagerSource) { - var solution = new TestdataMultiVarSolution("solution"); - var firstValue = new TestdataValue("firstValue"); - var secondValue = new TestdataValue("secondValue"); - solution.setValueList(List.of(firstValue, secondValue)); - var firstOtherValue = new TestdataOtherValue("firstOtherValue"); - solution.setOtherValueList(List.of(firstOtherValue)); - var uninitializedEntity = new TestdataMultiVarEntity("uninitialized"); - solution.setMultiVarEntityList(List.of(uninitializedEntity)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_MULTIVAR); - var recommendationList = solutionManager.recommendAssignment(solution, uninitializedEntity, - entity -> new Triple<>(entity.getPrimaryValue(), entity.getSecondaryValue(), - entity.getTertiaryValueAllowedUnassigned())); - assertThat(recommendationList).hasSize(8); - - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - var propositition = firstRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(firstRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - var propositition = secondRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(secondRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - var propositition = thirdRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-1)); - }); - - var fourthRecommendation = recommendationList.get(3); - assertSoftly(softly -> { - var propositition = fourthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(fourthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-1)); - }); - - var fifthRecommendation = recommendationList.get(4); - assertSoftly(softly -> { - var propositition = fifthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(fifthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-1)); - }); - - var sixthRecommendation = recommendationList.get(5); - assertSoftly(softly -> { - var propositition = sixthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(sixthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-1)); - }); - - var seventhRecommendation = recommendationList.get(6); - assertSoftly(softly -> { - var propositition = seventhRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(seventhRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-2)); - }); - - var eighthRecommendation = recommendationList.get(7); - assertSoftly(softly -> { - var propositition = eighthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(eighthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-2)); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentMultivarAlreadyAssigned(SolutionManagerSource SolutionManagerSource) { - var solution = new TestdataMultiVarSolution("solution"); - var firstValue = new TestdataValue("firstValue"); - var secondValue = new TestdataValue("secondValue"); - solution.setValueList(List.of(firstValue, secondValue)); - var firstOtherValue = new TestdataOtherValue("firstOtherValue"); - solution.setOtherValueList(List.of(firstOtherValue)); - var evaluatedEntity = new TestdataMultiVarEntity("uninitialized"); - evaluatedEntity.setPrimaryValue(firstValue); - evaluatedEntity.setSecondaryValue(secondValue); - evaluatedEntity.setTertiaryValueAllowedUnassigned(firstOtherValue); - solution.setMultiVarEntityList(List.of(evaluatedEntity)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_MULTIVAR); - var recommendationList = solutionManager.recommendAssignment(solution, evaluatedEntity, - entity -> new Triple<>(entity.getPrimaryValue(), entity.getSecondaryValue(), - entity.getTertiaryValueAllowedUnassigned())); - assertThat(recommendationList).hasSize(8); - - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - var propositition = firstRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(firstRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(2)); - }); - - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - var propositition = secondRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(secondRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(2)); - }); - - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - var propositition = thirdRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(1)); - }); - - var fourthRecommendation = recommendationList.get(3); - assertSoftly(softly -> { - var propositition = fourthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(fourthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(1)); - }); - - var fifthRecommendation = recommendationList.get(4); - assertSoftly(softly -> { - var propositition = fifthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isNull(); - softly.assertThat(fifthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(1)); - }); - - var sixthRecommendation = recommendationList.get(5); - assertSoftly(softly -> { - var propositition = sixthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(sixthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(1)); - }); - - var seventhRecommendation = recommendationList.get(6); - assertSoftly(softly -> { - var propositition = seventhRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(firstValue); - softly.assertThat(propositition.b()).isSameAs(secondValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(seventhRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - - var eighthRecommendation = recommendationList.get(7); - assertSoftly(softly -> { - var propositition = eighthRecommendation.proposition(); - softly.assertThat(propositition.a()).isSameAs(secondValue); - softly.assertThat(propositition.b()).isSameAs(firstValue); - softly.assertThat(propositition.c()).isSameAs(firstOtherValue); - softly.assertThat(eighthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentUninitializedSolutionWithMultivar(SolutionManagerSource SolutionManagerSource) { - var solution = new TestdataMultiVarSolution("solution"); - var firstValue = new TestdataValue("firstValue"); - var secondValue = new TestdataValue("secondValue"); - solution.setValueList(List.of(firstValue, secondValue)); - var firstOtherValue = new TestdataOtherValue("firstOtherValue"); - solution.setOtherValueList(List.of(firstOtherValue)); - var uninitializedEntity = new TestdataMultiVarEntity("uninitialized"); - var secondUninitializedEntity = new TestdataMultiVarEntity("uninitialized2"); - solution.setMultiVarEntityList(List.of(uninitializedEntity, secondUninitializedEntity)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_MULTIVAR); - assertThatThrownBy(() -> solutionManager.recommendAssignment(solution, uninitializedEntity, - entity -> new Triple<>(entity.getPrimaryValue(), entity.getSecondaryValue(), - entity.getTertiaryValueAllowedUnassigned()))) - .hasMessageContaining("Solution (solution) has (2) uninitialized elements."); - } - - record Triple(A a, B b, C c) { - - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentList(SolutionManagerSource SolutionManagerSource) { - var a = new TestdataListEntityWithShadowHistory("a"); - var b0 = new TestdataListValueWithShadowHistory("b0"); - var b = new TestdataListEntityWithShadowHistory("b", b0); - var c0 = new TestdataListValueWithShadowHistory("c0"); - var c1 = new TestdataListValueWithShadowHistory("c1"); - var c = new TestdataListEntityWithShadowHistory("c", c0, c1); - var solution = new TestdataListSolutionWithShadowHistory(); - TestdataListValueWithShadowHistory uninitializedValue = new TestdataListValueWithShadowHistory("uninitialized"); - solution.setEntityList(Arrays.asList(a, b, c)); - solution.setValueList(Arrays.asList(b0, c0, c1, uninitializedValue)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST); - var recommendationList = - solutionManager.recommendAssignment(solution, uninitializedValue, v -> new Pair<>(v.getEntity(), v.getIndex())); - assertThat(recommendationList).hasSize(6); - - // First recommendation is to be added to the "a" list variable, as that results in the shortest list. - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - var result = firstRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - // The entity is cloned... - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(a); - softly.assertThat(entity.getCode()).isEqualTo(a.getCode()); - // ... but it is in a state as it would've been in the original solution. - softly.assertThat(entity.getValueList()).isEmpty(); - softly.assertThat(firstRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-1)); - }); - - // Second recommendation is to be added to the start of the "b" list variable. - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - var result = secondRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(b); - softly.assertThat(entity.getCode()).isEqualTo(b.getCode()); - softly.assertThat(entity.getValueList()).hasSize(1); - softly.assertThat(secondRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-3)); - }); - - // Third recommendation is to be added to the end of the "b" list variable. - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - var result = thirdRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(1); // End of the list. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(b); - softly.assertThat(entity.getCode()).isEqualTo(b.getCode()); - softly.assertThat(entity.getValueList()).hasSize(1); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-3)); - }); - - // Fourth recommendation is to be added to the "c" list variable and so on... - var fourthRecommendation = recommendationList.get(3); - assertSoftly(softly -> { - var result = fourthRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - var entity = result.key(); - softly.assertThat(entity.getCode()).isNotEqualTo(c); - softly.assertThat(entity.getCode()).isEqualTo(c.getCode()); - softly.assertThat(entity.getValueList()).hasSize(2); - softly.assertThat(fourthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-5)); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentListAlreadyAssigned(SolutionManagerSource SolutionManagerSource) { - var a = new TestdataListEntityWithShadowHistory("a"); - var b0 = new TestdataListValueWithShadowHistory("b0"); - var b = new TestdataListEntityWithShadowHistory("b", b0); - var c0 = new TestdataListValueWithShadowHistory("c0"); - var evaluatedValue = new TestdataListValueWithShadowHistory("c1"); - var c = new TestdataListEntityWithShadowHistory("c", c0, evaluatedValue); - var solution = new TestdataListSolutionWithShadowHistory(); - solution.setEntityList(Arrays.asList(a, b, c)); - solution.setValueList(Arrays.asList(b0, c0, evaluatedValue)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST); - var recommendationList = - solutionManager.recommendAssignment(solution, evaluatedValue, v -> new Pair<>(v.getEntity(), v.getIndex())); - assertThat(recommendationList).hasSize(5); - - // First recommendation is to be added to the "a" list variable, as that results in the shortest list. - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - var result = firstRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - // The entity is cloned... - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(a); - softly.assertThat(entity.getCode()).isEqualTo(a.getCode()); - // ... but it is in a state as it would've been in the original solution. - softly.assertThat(entity.getValueList()).isEmpty(); - softly.assertThat(firstRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(2)); - }); - - // Second recommendation is to be added to the start of the "b" list variable. - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - var result = secondRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(b); - softly.assertThat(entity.getCode()).isEqualTo(b.getCode()); - softly.assertThat(entity.getValueList()).hasSize(1); - softly.assertThat(secondRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - - // Third recommendation is to be added to the beginning of the "c" list variable. - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - var result = thirdRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(c); - softly.assertThat(entity.getCode()).isEqualTo(c.getCode()); - softly.assertThat(entity.getValueList()).hasSize(1); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - - // Fourth recommendation is to be added to the "b" list variable and so on... - var fourthRecommendation = recommendationList.get(3); - assertSoftly(softly -> { - var result = fourthRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(1); // End of the list. - var entity = result.key(); - softly.assertThat(entity.getCode()).isNotEqualTo(b); - softly.assertThat(entity.getCode()).isEqualTo(b.getCode()); - softly.assertThat(entity.getValueList()).hasSize(1); - softly.assertThat(fourthRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentTwoUninitializedEntityWithList(SolutionManagerSource SolutionManagerSource) { - var a = new TestdataListEntityWithShadowHistory("a"); - var b0 = new TestdataListValueWithShadowHistory("b0"); - var b = new TestdataListEntityWithShadowHistory("b", b0); - var c0 = new TestdataListValueWithShadowHistory("c0"); - var c1 = new TestdataListValueWithShadowHistory("c1"); - var c = new TestdataListEntityWithShadowHistory("c", c0, c1); - var d = new TestdataListEntityWithShadowHistory("d"); - var solution = new TestdataListSolutionWithShadowHistory(); - TestdataListValueWithShadowHistory uninitializedValue = new TestdataListValueWithShadowHistory("uninitialized"); - TestdataListValueWithShadowHistory uninitializedValue2 = new TestdataListValueWithShadowHistory("uninitialized2"); - solution.setEntityList(Arrays.asList(a, b, c, d)); - solution.setValueList(Arrays.asList(b0, c0, c1, uninitializedValue, uninitializedValue2)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST); - assertThatThrownBy(() -> solutionManager.recommendAssignment(solution, uninitializedValue, - TestdataListValueWithShadowHistory::getEntity)) - .hasMessageContaining("at most one"); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentListPinned(SolutionManagerSource SolutionManagerSource) { - var a = new TestdataPinnedWithIndexListEntity("a"); - var b0 = new TestdataPinnedWithIndexListValue("b0"); - var b = new TestdataPinnedWithIndexListEntity("b", b0); - b.setPinned(true); // Entity will be unavailable. - var c0 = new TestdataPinnedWithIndexListValue("c0"); - var c1 = new TestdataPinnedWithIndexListValue("c1"); - var c = new TestdataPinnedWithIndexListEntity("c", c0, c1); - c.setPinned(false); - c.setPinIndex(1); // Destination c[0] will be unavailable. - var solution = new TestdataPinnedWithIndexListSolution(); - var uninitializedValue = new TestdataPinnedWithIndexListValue("uninitialized"); - solution.setEntityList(Arrays.asList(a, b, c)); - solution.setValueList(Arrays.asList(b0, c0, c1, uninitializedValue)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST_PINNED); - var recommendationList = - solutionManager.recommendAssignment(solution, uninitializedValue, - v -> new Pair<>(v.getEntity(), v.getEntity().getValueList().indexOf(v))); - assertThat(recommendationList).hasSize(3); - - // First recommendation is to be added to the "a" list variable, as that results in the shortest list. - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - var result = firstRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - // The entity is cloned... - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(a); - softly.assertThat(entity.getCode()).isEqualTo(a.getCode()); - // ... but it is in a state as it would've been in the original solution. - softly.assertThat(entity.getValueList()).isEmpty(); - softly.assertThat(firstRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-1)); - }); - - // Second recommendation is to be added to c[1]. - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - var result = secondRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(1); // First unpinned index. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(c); - softly.assertThat(entity.getCode()).isEqualTo(c.getCode()); - softly.assertThat(entity.getValueList()).hasSize(2); - softly.assertThat(secondRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-5)); - }); - - // Third recommendation is to be added to c[2]. - var thirdRecommendation = recommendationList.get(2); - assertSoftly(softly -> { - var result = thirdRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(2); // End of the list. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(c); - softly.assertThat(entity.getCode()).isEqualTo(c.getCode()); - softly.assertThat(entity.getValueList()).hasSize(2); - softly.assertThat(thirdRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(-5)); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentListPinnedAlreadyInitialized(SolutionManagerSource SolutionManagerSource) { - var a = new TestdataPinnedWithIndexListEntity("a"); - var b0 = new TestdataPinnedWithIndexListValue("b0"); - var b = new TestdataPinnedWithIndexListEntity("b", b0); - b.setPinned(true); // Entity will be unavailable. - var c0 = new TestdataPinnedWithIndexListValue("c0"); - var evaluatedValue = new TestdataPinnedWithIndexListValue("c1"); - var c = new TestdataPinnedWithIndexListEntity("c", c0, evaluatedValue); - c.setPinned(false); - c.setPinIndex(1); // Destination c[0] will be unavailable. - var solution = new TestdataPinnedWithIndexListSolution(); - solution.setEntityList(Arrays.asList(a, b, c)); - solution.setValueList(Arrays.asList(b0, c0, evaluatedValue)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST_PINNED); - var recommendationList = - solutionManager.recommendAssignment(solution, evaluatedValue, - v -> new Pair<>(v.getEntity(), v.getEntity().getValueList().indexOf(v))); - assertThat(recommendationList).hasSize(2); - - // First recommendation is to be added to the "a" list variable, as that results in the shortest list. - var firstRecommendation = recommendationList.get(0); - assertSoftly(softly -> { - var result = firstRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(0); // Beginning of the list. - // The entity is cloned... - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(a); - softly.assertThat(entity.getCode()).isEqualTo(a.getCode()); - // ... but it is in a state as it would've been in the original solution. - softly.assertThat(entity.getValueList()).isEmpty(); - softly.assertThat(firstRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(2)); - }); - - // Second recommendation is to be added to c[1]. - var secondRecommendation = recommendationList.get(1); - assertSoftly(softly -> { - var result = secondRecommendation.proposition(); - softly.assertThat(result.value()).isEqualTo(1); // First unpinned index. - var entity = result.key(); - softly.assertThat(entity).isNotEqualTo(c); - softly.assertThat(entity.getCode()).isEqualTo(c.getCode()); - softly.assertThat(entity.getValueList()).hasSize(1); - softly.assertThat(secondRecommendation.scoreAnalysisDiff().score()).isEqualTo(SimpleScore.of(0)); - }); - } - - @ParameterizedTest - @EnumSource(SolutionManagerSource.class) - void recommendAssignmentTwoUninitializedEntityWithListPinned(SolutionManagerSource SolutionManagerSource) { - var a = new TestdataPinnedWithIndexListEntity("a"); - var b0 = new TestdataPinnedWithIndexListValue("b0"); - var b = new TestdataPinnedWithIndexListEntity("b", b0); - b.setPinned(true); // Entity will be unavailable. - var c0 = new TestdataPinnedWithIndexListValue("c0"); - var c1 = new TestdataPinnedWithIndexListValue("c1"); - var c = new TestdataPinnedWithIndexListEntity("c", c0, c1); - var d = new TestdataPinnedWithIndexListEntity("d"); - c.setPinned(false); - c.setPinIndex(1); // Destination c[0] will be unavailable. - var solution = new TestdataPinnedWithIndexListSolution(); - var uninitializedValue = new TestdataPinnedWithIndexListValue("uninitialized"); - solution.setEntityList(Arrays.asList(a, b, c, d)); - solution.setValueList(Arrays.asList(b0, c0, c1, uninitializedValue)); - - var solutionManager = SolutionManagerSource.createSolutionManager(SOLVER_FACTORY_LIST_PINNED); - var recommendationList = - solutionManager.recommendAssignment(solution, uninitializedValue, - v -> new Pair<>(v.getEntity(), v.getEntity().getValueList().indexOf(v))); - assertThat(recommendationList).hasSize(4); - } - @SuppressWarnings("unchecked") @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void visualizeNodeNetwork(SolutionManagerSource SolutionManagerSource) { + void visualizeNodeNetwork(SolutionManagerSource solutionManagerSource) { var solution = new TestdataSolution(); - var solutionManager = (DefaultSolutionManager) SolutionManagerSource + var solutionManager = (DefaultSolutionManager) solutionManagerSource .createSolutionManager(SOLVER_FACTORY_WITH_CS); var result = solutionManager.visualizeNodeNetwork(solution); assertThat(result).isEqualToIgnoringWhitespace( @@ -1354,10 +376,10 @@ void visualizeNodeNetwork(SolutionManagerSource SolutionManagerSource) { @SuppressWarnings("unchecked") @ParameterizedTest @EnumSource(SolutionManagerSource.class) - void visualizeNodeNetworkNoBavet(SolutionManagerSource SolutionManagerSource) { - var solution = new TestdataPinnedWithIndexListSolution(); - var solutionManager = (DefaultSolutionManager) SolutionManagerSource - .createSolutionManager(SOLVER_FACTORY_LIST_PINNED); + void visualizeNodeNetworkNoBavet(SolutionManagerSource solutionManagerSource) { + var solution = new TestdataSolution(); + var solutionManager = (DefaultSolutionManager) solutionManagerSource + .createSolutionManager(SOLVER_FACTORY_EASY); assertThatThrownBy(() -> solutionManager.visualizeNodeNetwork(solution)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("Constraint Streams"); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java index ba4a6726c40..a99923144a5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java @@ -7,7 +7,7 @@ import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/PlanningSolutionDiffTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/PlanningSolutionDiffTest.java deleted file mode 100644 index 27c1be4a33a..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/PlanningSolutionDiffTest.java +++ /dev/null @@ -1,317 +0,0 @@ -package ai.timefold.solver.core.impl.domain.solution.descriptor; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.solver.SolutionManager; -import ai.timefold.solver.core.api.solver.SolverFactory; -import ai.timefold.solver.core.config.solver.PreviewFeature; -import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; -import ai.timefold.solver.core.testdomain.TestdataEntity; -import ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeEasyScoreCalculator; -import ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeEntity; -import ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeSolution; -import ai.timefold.solver.core.testdomain.equals.list.TestdataEqualsByCodeListEasyScoreCalculator; -import ai.timefold.solver.core.testdomain.equals.list.TestdataEqualsByCodeListEntity; -import ai.timefold.solver.core.testdomain.equals.list.TestdataEqualsByCodeListSolution; -import ai.timefold.solver.core.testdomain.equals.list.TestdataEqualsByCodeListValue; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class PlanningSolutionDiffTest { - - @Nested - @DisplayName("Diff of two solutions called without the preview feature enabled") - class PlanningSolutionDiffPreviewNotEnabledTest { - - private final SolutionManager solutionManager = - SolutionManager.create(SolverFactory.create(new SolverConfig() - .withSolutionClass(TestdataEqualsByCodeSolution.class) - .withEntityClasses(TestdataEqualsByCodeEntity.class) - .withEasyScoreCalculatorClass(TestdataEqualsByCodeEasyScoreCalculator.class))); - - @Test - void failsFast() { - assertThatThrownBy( - () -> solutionManager.diff(new TestdataEqualsByCodeSolution(), new TestdataEqualsByCodeSolution())) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining(PreviewFeature.PLANNING_SOLUTION_DIFF.name()); - } - - } - - @Nested - @DisplayName("Diff of two solutions of the same class with a single basic variable") - class BasicVariablePlanningSolutionDiffTest { - - private final SolutionManager solutionManager = - SolutionManager.create(SolverFactory.create(new SolverConfig() - .withPreviewFeature(PreviewFeature.PLANNING_SOLUTION_DIFF) - .withSolutionClass(TestdataEqualsByCodeSolution.class) - .withEntityClasses(TestdataEqualsByCodeEntity.class) - .withEasyScoreCalculatorClass(TestdataEqualsByCodeEasyScoreCalculator.class))); - - @Nested - @DisplayName("Where the two solutions have identical contents") - class BasicVariablePlanningSolutionDiffIdenticalContentsTest { - - private final TestdataEqualsByCodeSolution oldSolution = - TestdataEqualsByCodeSolution.generateSolution("Old Solution", 2, 10); - private final TestdataEqualsByCodeSolution newSolution = - TestdataEqualsByCodeSolution.generateSolution("New Solution", 2, 10); - private final PlanningSolutionDiff diff = - solutionManager.diff(oldSolution, newSolution); - - @Test - void hasFieldsSet() { - assertSoftly(softly -> { - softly.assertThat(diff.solutionMetaModel()).isNotNull(); - softly.assertThat(diff.addedEntities()).isEmpty(); - softly.assertThat(diff.removedEntities()).isEmpty(); - softly.assertThat(diff.oldSolution()).isSameAs(oldSolution); - softly.assertThat(diff.newSolution()).isSameAs(newSolution); - }); - } - - @Test - void providesNoEntityDiffs() { - assertSoftly(softly -> { - softly.assertThat(diff.entityDiff(newSolution.getEntityList().get(0))).isNull(); - softly.assertThat(diff.entityDiff(oldSolution.getEntityList().get(0))).isNull(); - softly.assertThat(diff.entityDiffs()).isEmpty(); - softly.assertThat(diff.entityDiffs(TestdataEntity.class)).isEmpty(); - softly.assertThat(diff.entityDiffs(TestdataEqualsByCodeEntity.class)).isEmpty(); - }); - } - - @Test - void hasHelpfulToString() { - assertThat(diff.toString()) - .isEqualToIgnoringWhitespace( - """ - Difference(s) between old planning solution (Old Solution) and new planning solution (New Solution): - - - Old solution entities not present in new solution: - (None.) - - - New solution entities not present in old solution: - (None.) - - - Entities changed between the solutions: - (None.) - """); - } - } - - @Nested - @DisplayName("Where the two solutions have different contents") - class BasicVariablePlanningSolutionDiffDifferentEntitiesTest { - - private final TestdataEqualsByCodeSolution oldSolution = - TestdataEqualsByCodeSolution.generateSolution("Old Solution", 2, 10); - private final TestdataEqualsByCodeEntity oldEntityRemoved = oldSolution.getEntityList().remove(0); - private final TestdataEqualsByCodeSolution newSolution = - TestdataEqualsByCodeSolution.generateSolution("New Solution", 2, 10); - private final TestdataEqualsByCodeEntity newEntityRemoved = newSolution.getEntityList().remove(9); - private PlanningSolutionDiff diff; - - @BeforeEach - void beforeEach() { - newSolution.getEntityList().forEach(entity -> { - var newValue = entity.getValue() == newSolution.getValueList().get(0) ? newSolution.getValueList().get(1) - : newSolution.getValueList().get(0); - entity.setValue(newValue); - }); - diff = solutionManager.diff(oldSolution, newSolution); - } - - @Test - void hasFieldsSet() { - assertSoftly(softly -> { - softly.assertThat(diff.solutionMetaModel()).isNotNull(); - softly.assertThat(diff.addedEntities()).containsExactly(oldEntityRemoved); - softly.assertThat(diff.removedEntities()).containsExactly(newEntityRemoved); - softly.assertThat(diff.oldSolution()).isSameAs(oldSolution); - softly.assertThat(diff.newSolution()).isSameAs(newSolution); - }); - } - - @Test - void providesEntityDiffs() { // 8 entities are different. - assertSoftly(softly -> { - softly.assertThat(diff.entityDiff(oldEntityRemoved)).isNull(); - softly.assertThat(diff.entityDiff(newEntityRemoved)).isNull(); - softly.assertThat(diff.entityDiffs()).hasSize(8); - softly.assertThat(diff.entityDiffs(TestdataEntity.class)).isEmpty(); - softly.assertThat(diff.entityDiffs(TestdataEqualsByCodeEntity.class)).hasSize(8); - }); - } - - @Test - void hasHelpfulToString() { - assertThat(diff.toString()) - .isEqualToIgnoringWhitespace( - """ - Difference(s) between old planning solution (Old Solution) and new planning solution (New Solution): - - - Old solution entities not present in new solution: - Generated Entity 9 - - - New solution entities not present in old solution: - Generated Entity 0 - - - Entities changed between the solutions: - Generated Entity 1 (Generated Value 1 -> Generated Value 0) - Generated Entity 2 (Generated Value 0 -> Generated Value 1) - Generated Entity 3 (Generated Value 1 -> Generated Value 0) - Generated Entity 4 (Generated Value 0 -> Generated Value 1) - Generated Entity 5 (Generated Value 1 -> Generated Value 0) - ... - """); - } - } - - } - - @Nested - @DisplayName("Diff of two solutions of the same class with a list variable") - class ListVariablePlanningSolutionDiffTest { - - private final SolutionManager solutionManager = - SolutionManager.create(SolverFactory.create(new SolverConfig() - .withPreviewFeature(PreviewFeature.PLANNING_SOLUTION_DIFF) - .withSolutionClass(TestdataEqualsByCodeListSolution.class) - .withEntityClasses(TestdataEqualsByCodeListEntity.class, TestdataEqualsByCodeListValue.class) - .withEasyScoreCalculatorClass(TestdataEqualsByCodeListEasyScoreCalculator.class))); - - @Nested - @DisplayName("Where the two solutions have identical contents") - class ListVariablePlanningSolutionDiffIdenticalContentsTest { - - private final TestdataEqualsByCodeListSolution oldSolution = - TestdataEqualsByCodeListSolution.generateSolution("Old Solution", 2, 10) - .initialize(); - private final TestdataEqualsByCodeListSolution newSolution = - TestdataEqualsByCodeListSolution.generateSolution("New Solution", 2, 10) - .initialize(); - private final PlanningSolutionDiff diff = - solutionManager.diff(oldSolution, newSolution); - - @Test - void hasFieldsSet() { - assertSoftly(softly -> { - softly.assertThat(diff.solutionMetaModel()).isNotNull(); - softly.assertThat(diff.addedEntities()).isEmpty(); - softly.assertThat(diff.removedEntities()).isEmpty(); - softly.assertThat(diff.oldSolution()).isSameAs(oldSolution); - softly.assertThat(diff.newSolution()).isSameAs(newSolution); - }); - } - - @Test - void providesNoEntityDiffs() { - assertSoftly(softly -> { - softly.assertThat(diff.entityDiff(newSolution.getEntityList().get(0))).isNull(); - softly.assertThat(diff.entityDiff(oldSolution.getEntityList().get(0))).isNull(); - softly.assertThat(diff.entityDiffs()).isEmpty(); - softly.assertThat(diff.entityDiffs(TestdataEntity.class)).isEmpty(); - softly.assertThat(diff.entityDiffs(TestdataEqualsByCodeEntity.class)).isEmpty(); - }); - } - - @Test - void hasHelpfulToString() { - assertThat(diff.toString()) - .isEqualToIgnoringWhitespace( - """ - Difference(s) between old planning solution (Old Solution) and new planning solution (New Solution): - - - Old solution entities not present in new solution: - (None.) - - - New solution entities not present in old solution: - (None.) - - - Entities changed between the solutions: - (None.) - """); - } - } - - @Nested - @DisplayName("Where the two solutions have different contents") - class ListVariablePlanningSolutionDiffDifferentEntitiesTest { - - private final TestdataEqualsByCodeListSolution oldSolution = - TestdataEqualsByCodeListSolution.generateSolution("Old Solution", 3, 10); - private final TestdataEqualsByCodeListEntity oldEntityRemoved = oldSolution.getEntityList().remove(1); - private final TestdataEqualsByCodeListValue oldValueRemoved = oldSolution.getValueList().remove(0); - private final TestdataEqualsByCodeListSolution newSolution = - TestdataEqualsByCodeListSolution.generateSolution("New Solution", 3, 10); - private final TestdataEqualsByCodeListEntity newEntityRemoved = newSolution.getEntityList().remove(9); - private final TestdataEqualsByCodeListValue newValueRemoved = newSolution.getValueList().remove(2); - private PlanningSolutionDiff diff; - - @BeforeEach - void beforeEach() { - oldSolution.initialize(); - newSolution.initialize(); - diff = solutionManager.diff(oldSolution, newSolution); - } - - @Test - void hasFieldsSet() { - assertSoftly(softly -> { - softly.assertThat(diff.solutionMetaModel()).isNotNull(); - softly.assertThat(diff.addedEntities()).containsOnly(oldEntityRemoved, oldValueRemoved); - softly.assertThat(diff.removedEntities()).containsOnly(newEntityRemoved, newValueRemoved); - softly.assertThat(diff.oldSolution()).isSameAs(oldSolution); - softly.assertThat(diff.newSolution()).isSameAs(newSolution); - }); - } - - @Test - void providesEntityDiffs() { - assertSoftly(softly -> { - softly.assertThat(diff.entityDiff(oldEntityRemoved)).isNull(); - softly.assertThat(diff.entityDiff(newEntityRemoved)).isNull(); - softly.assertThat(diff.entityDiffs()).hasSize(3); - softly.assertThat(diff.entityDiffs(TestdataEqualsByCodeEntity.class)).isEmpty(); - softly.assertThat(diff.entityDiffs(TestdataEqualsByCodeListEntity.class)).hasSize(2); - softly.assertThat(diff.entityDiffs(TestdataEqualsByCodeListValue.class)).hasSize(1); - }); - } - - @Test - void hasHelpfulToString() { - assertThat(diff.toString()) - .isEqualToIgnoringWhitespace( - """ - Difference(s) between old planning solution (Old Solution) and new planning solution (New Solution): - - - Old solution entities not present in new solution: - Generated Entity 9 - Generated Value 2 - - - New solution entities not present in old solution: - Generated Entity 1 - Generated Value 0 - - - Entities changed between the solutions: - Generated Entity 0 ([Generated Value 1] -> [Generated Value 0]) - Generated Entity 2 ([Generated Value 2] -> []) - Generated Value 1: - entity (shadow): Generated Entity 0 -> Generated Entity 1 - """); - } - } - - } - -} diff --git a/core/src/test/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatchTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java similarity index 88% rename from core/src/test/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatchTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java index cf2c4366130..5102f4cd01b 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatchTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java @@ -1,12 +1,11 @@ -package ai.timefold.solver.core.api.score.constraint; +package ai.timefold.solver.core.impl.score.constraint; import static ai.timefold.solver.core.api.score.SimpleScore.ONE; import static ai.timefold.solver.core.api.score.SimpleScore.ZERO; -import java.util.Arrays; - import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleScore; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.testutil.PlannerAssert; @@ -24,10 +23,10 @@ void equalsAndHashCode() { // No CM should equal any other. } private > ConstraintMatch buildConstraintMatch(String constraintName, Score_ score, - Object... indictments) { + Object... facts) { return new ConstraintMatch<>(ConstraintRef.of(constraintName), - DefaultConstraintJustification.of(score, indictments), - Arrays.asList(indictments), score); + DefaultConstraintJustification.of(score, facts), + score); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotalTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotalTest.java similarity index 56% rename from core/src/test/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotalTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotalTest.java index 22575ed3203..2271c4257a3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotalTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotalTest.java @@ -5,22 +5,21 @@ import java.util.List; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testutil.PlannerAssert; import org.junit.jupiter.api.Test; -class DefaultConstraintMatchTotalTest { +class ConstraintMatchTotalTest { @Test void getScoreTotal() { TestdataEntity e1 = new TestdataEntity("e1"); TestdataEntity e2 = new TestdataEntity("e2"); TestdataEntity e3 = new TestdataEntity("e3"); - DefaultConstraintMatchTotal constraintMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of("constraint1"), SimpleScore.ZERO); + ConstraintMatchTotal constraintMatchTotal = + new ConstraintMatchTotal<>(ConstraintRef.of("constraint1"), SimpleScore.ZERO); assertThat(constraintMatchTotal.getScore()).isEqualTo(SimpleScore.ZERO); ConstraintMatch match1 = @@ -45,25 +44,25 @@ void getScoreTotal() { @Test void equalsAndHashCode() { PlannerAssert.assertObjectsAreEqual( - new DefaultConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.of(-7))); + new ConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.of(-7))); PlannerAssert.assertObjectsAreNotEqual( - new DefaultConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("d"), SimpleScore.ZERO)); + new ConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("d"), SimpleScore.ZERO)); } @Test void compareTo() { PlannerAssert.assertCompareToOrder( - new DefaultConstraintMatchTotal<>(ConstraintRef.of("a"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("aa"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("ab"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("b"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("ca"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("cb"), SimpleScore.ZERO), - new DefaultConstraintMatchTotal<>(ConstraintRef.of("d"), SimpleScore.ZERO)); + new ConstraintMatchTotal<>(ConstraintRef.of("a"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("aa"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("ab"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("b"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("c"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("ca"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("cb"), SimpleScore.ZERO), + new ConstraintMatchTotal<>(ConstraintRef.of("d"), SimpleScore.ZERO)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictmentTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictmentTest.java deleted file mode 100644 index adaba22d78a..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictmentTest.java +++ /dev/null @@ -1,143 +0,0 @@ -package ai.timefold.solver.core.impl.score.constraint; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Arrays; -import java.util.List; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; -import ai.timefold.solver.core.testdomain.TestdataEntity; -import ai.timefold.solver.core.testutil.PlannerAssert; - -import org.junit.jupiter.api.Test; - -class DefaultIndictmentTest { - - @Test - void getScoreTotal() { - TestdataEntity e1 = new TestdataEntity("e1"); - TestdataEntity e2 = new TestdataEntity("e2"); - TestdataEntity e3 = new TestdataEntity("e3"); - DefaultIndictment indictment = new DefaultIndictment<>(e1, SimpleScore.ZERO); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.ZERO); - - ConstraintMatch match1 = buildConstraintMatch("constraint1", SimpleScore.of(-1), e1); - indictment.addConstraintMatch(match1); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-1)); - // Different constraintName - ConstraintMatch match2 = buildConstraintMatch("constraint2", SimpleScore.of(-20), e1); - indictment.addConstraintMatch(match2); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-21)); - indictment.addConstraintMatch(buildConstraintMatch("constraint3", SimpleScore.of(-300), e1, e2)); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-321)); - // Different justification - indictment.addConstraintMatch(buildConstraintMatch("constraint3", SimpleScore.of(-4000), e1, e3)); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-4321)); - // Almost duplicate, but e2 and e1 are in reverse order, so different justification - indictment.addConstraintMatch(buildConstraintMatch("constraint3", SimpleScore.of(-50000), e2, e1)); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-54321)); - - indictment.removeConstraintMatch(match2); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-54301)); - indictment.removeConstraintMatch(match1); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.of(-54300)); - } - - @Test - void getJustificationList() { - TestdataEntity e1 = new TestdataEntity("e1"); - TestdataEntity e2 = new TestdataEntity("e2"); - TestdataEntity e3 = new TestdataEntity("e3"); - DefaultIndictment indictment = new DefaultIndictment<>(e1, SimpleScore.ZERO); - assertThat(indictment.getScore()).isEqualTo(SimpleScore.ZERO); - - // Add a constraint match with a default justification - ConstraintMatch match1 = buildConstraintMatch("constraint1", SimpleScore.of(-1), e1, e2); - indictment.addConstraintMatch(match1); - - assertThat(indictment.getJustificationList()) - .hasSize(1); - DefaultConstraintJustification constraintJustification = - (DefaultConstraintJustification) indictment.getJustificationList().get(0); - assertThat(constraintJustification.getFacts()) - .containsExactly(e1, e2); - - assertThat(indictment.getJustificationList(DefaultConstraintJustification.class)) - .hasSize(1); - constraintJustification = indictment.getJustificationList(DefaultConstraintJustification.class).get(0); - assertThat(constraintJustification.getFacts()) - .containsExactly(e1, e2); - - // Add another constraint match with a custom justification - ConstraintMatch match2 = buildConstraintMatch("constraint1", - SimpleScore.of(-1), new TestConstraintJustification(e1, e3), e1, e3); - indictment.addConstraintMatch(match2); - - assertThat(indictment.getJustificationList()) - .hasSize(2); - DefaultConstraintJustification firstConstraintJustification = - (DefaultConstraintJustification) indictment.getJustificationList().get(0); - assertThat(firstConstraintJustification.getFacts()) - .containsExactly(e1, e2); - TestConstraintJustification secondConstraintJustification = - (TestConstraintJustification) indictment.getJustificationList().get(1); - assertThat(secondConstraintJustification.getFacts()) - .containsExactly(e1, e3); - - assertThat(indictment.getJustificationList(DefaultConstraintJustification.class)) - .hasSize(1); - firstConstraintJustification = indictment.getJustificationList(DefaultConstraintJustification.class).get(0); - assertThat(firstConstraintJustification.getFacts()) - .containsExactly(e1, e2); - - assertThat(indictment.getJustificationList(TestConstraintJustification.class)) - .hasSize(1); - secondConstraintJustification = indictment.getJustificationList(TestConstraintJustification.class).get(0); - assertThat(secondConstraintJustification.getFacts()) - .containsExactly(e1, e3); - } - - private > ConstraintMatch buildConstraintMatch(String constraintName, Score_ score, - Object... indictments) { - return buildConstraintMatch(constraintName, score, - DefaultConstraintJustification.of(score, indictments), indictments); - } - - private > ConstraintMatch buildConstraintMatch(String constraintName, Score_ score, - ConstraintJustification justification, Object... indictments) { - return new ConstraintMatch<>(ConstraintRef.of(constraintName), justification, - Arrays.asList(indictments), score); - } - - @Test - void equalsAndHashCode() { - PlannerAssert.assertObjectsAreEqual( - new DefaultIndictment<>("e1", SimpleScore.ZERO), - new DefaultIndictment<>("e1", SimpleScore.ZERO), - new DefaultIndictment<>("e1", SimpleScore.of(-7))); - PlannerAssert.assertObjectsAreNotEqual( - new DefaultIndictment<>("a", SimpleScore.ZERO), - new DefaultIndictment<>("aa", SimpleScore.ZERO), - new DefaultIndictment<>("b", SimpleScore.ZERO), - new DefaultIndictment<>("c", SimpleScore.ZERO)); - } - - private final class TestConstraintJustification implements ConstraintJustification { - - private final List facts; - - public TestConstraintJustification(Object... facts) { - this.facts = Arrays.asList(facts); - } - - public List getFacts() { - return facts; - } - } - -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java index 245807fb877..4565e4084af 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.score.director; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.util.List; import java.util.Map; @@ -10,16 +9,16 @@ import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; +import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesSolution; import ai.timefold.solver.core.testdomain.list.pinned.TestdataPinnedListSolution; import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListSolution; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; public abstract class AbstractScoreDirectorSemanticsTest { - private final SolutionDescriptor constraintConfigurationSolutionDescriptor = + protected final SolutionDescriptor constraintConfigurationSolutionDescriptor = TestdataConstraintWeightOverridesSolution.buildSolutionDescriptor(); private final SolutionDescriptor pinnedListSolutionDescriptor = TestdataPinnedListSolution.buildSolutionDescriptor(); @@ -87,22 +86,24 @@ void solutionBasedScoreWeights() { // Create score director, calculate score. var solution1 = TestdataConstraintWeightOverridesSolution.generateSolution(1, 1); - try (var scoreDirector = scoreDirectorFactory.buildScoreDirector()) { + try (var scoreDirector = scoreDirectorFactory.createScoreDirectorBuilder() + .withConstraintMatchPolicy( + scoreDirectorFactory instanceof EasyScoreDirectorFactory ? ConstraintMatchPolicy.DISABLED + : ConstraintMatchPolicy.ENABLED) + .build()) { scoreDirector.setWorkingSolution(solution1); var score1 = scoreDirector.calculateScore(); assertThat(score1.raw()).isEqualTo(SimpleScore.ONE); // Set new solution with a different constraint weight, calculate score. var solution2 = TestdataConstraintWeightOverridesSolution.generateSolution(1, 1); - solution2.setConstraintWeightOverrides( - ConstraintWeightOverrides.of(Map.of("First weight", SimpleScore.of(2)))); + solution2.setConstraintWeightOverrides(ConstraintWeightOverrides.of(Map.of("First weight", SimpleScore.of(2)))); scoreDirector.setWorkingSolution(solution2); var score2 = scoreDirector.calculateScore(); assertThat(score2.raw()).isEqualTo(SimpleScore.of(2)); // Set new solution with a disabled constraint, calculate score. - solution2.setConstraintWeightOverrides( - ConstraintWeightOverrides.of(Map.of("First weight", SimpleScore.ZERO))); + solution2.setConstraintWeightOverrides(ConstraintWeightOverrides.of(Map.of("First weight", SimpleScore.ZERO))); scoreDirector.setWorkingSolution(solution2); var score3 = scoreDirector.calculateScore(); assertThat(score3.raw()).isEqualTo(SimpleScore.ZERO); @@ -117,7 +118,11 @@ void mutableConstraintConfiguration() { // Create score director, calculate score with a given constraint configuration. var solution = TestdataConstraintWeightOverridesSolution.generateSolution(1, 1); - try (var scoreDirector = scoreDirectorFactory.buildScoreDirector()) { + try (var scoreDirector = scoreDirectorFactory.createScoreDirectorBuilder() + .withConstraintMatchPolicy( + scoreDirectorFactory instanceof EasyScoreDirectorFactory ? ConstraintMatchPolicy.DISABLED + : ConstraintMatchPolicy.ENABLED) + .build()) { scoreDirector.setWorkingSolution(solution); var score1 = scoreDirector.calculateScore(); assertThat(score1.raw()).isEqualTo(SimpleScore.ONE); @@ -125,56 +130,19 @@ void mutableConstraintConfiguration() { // Change constraint configuration on the current working solution. var weightOverrides = solution.getConstraintWeightOverrides(); scoreDirector.beforeProblemPropertyChanged(weightOverrides); - solution.setConstraintWeightOverrides( - ConstraintWeightOverrides.of(Map.of("First weight", SimpleScore.of(2)))); + solution.setConstraintWeightOverrides(ConstraintWeightOverrides.of(Map.of("First weight", SimpleScore.of(2)))); scoreDirector.afterProblemPropertyChanged(weightOverrides); var score2 = scoreDirector.calculateScore(); assertThat(score2.raw()).isEqualTo(SimpleScore.of(2)); } } - @Test - void constraintPresentEvenIfNoMatches() { - var scoreDirectorFactory = - buildScoreDirectorFactoryWithConstraintConfiguration(constraintConfigurationSolutionDescriptor); - // Need constraint match support for this. - Assumptions.assumeTrue(scoreDirectorFactory.supportsConstraintMatching()); - - // Create score director, calculate score with a given constraint configuration. - var solution = TestdataConstraintWeightOverridesSolution.generateSolution(1, 1); - try (var scoreDirector = scoreDirectorFactory.createScoreDirectorBuilder() - .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) - .build()) { - scoreDirector.setWorkingSolution(solution); - var score1 = scoreDirector.calculateScore(); - assertSoftly(softly -> { - softly.assertThat(score1.isFullyAssigned()).isTrue(); - softly.assertThat(score1.raw().score()).isEqualTo(1); - softly.assertThat(scoreDirector.getConstraintMatchTotalMap()) - .containsOnlyKeys("First weight"); - }); - - // Make sure nothing matches, but the constraint is still present. - var entity = scoreDirector.getWorkingSolution().getEntityList().get(0); - scoreDirector.beforeVariableChanged(entity, "value"); - entity.setValue(null); - scoreDirector.afterVariableChanged(entity, "value"); - var score2 = scoreDirector.calculateScore(); - assertSoftly(softly -> { - softly.assertThat(score2.isFullyAssigned()).isFalse(); - softly.assertThat(score2.raw().score()).isZero(); - softly.assertThat(scoreDirector.getConstraintMatchTotalMap()) - .containsOnlyKeys("First weight"); - }); - } - } - @Test void listVariableEntityPinningSupported() { var scoreDirectorFactory = buildScoreDirectorFactoryWithListVariableEntityPin(pinnedListSolutionDescriptor); var solution = TestdataPinnedListSolution.generateUninitializedSolution(2, 2); - var firstEntity = solution.getEntityList().get(0); - firstEntity.setValueList(List.of(solution.getValueList().get(0))); + var firstEntity = solution.getEntityList().getFirst(); + firstEntity.setValueList(List.of(solution.getValueList().getFirst())); firstEntity.setPinned(true); try (var scoreDirector = scoreDirectorFactory.buildScoreDirector()) { @@ -195,11 +163,10 @@ void listVariableEntityPinningSupported() { @Test void listVariableIndexPinningSupported() { - var scoreDirectorFactory = - buildScoreDirectorFactoryWithListVariablePinIndex(pinnedWithIndexListSolutionDescriptor); + var scoreDirectorFactory = buildScoreDirectorFactoryWithListVariablePinIndex(pinnedWithIndexListSolutionDescriptor); var solution = TestdataPinnedWithIndexListSolution.generateUninitializedSolution(3, 3); - var firstEntity = solution.getEntityList().get(0); - firstEntity.setValueList(List.of(solution.getValueList().get(0))); + var firstEntity = solution.getEntityList().getFirst(); + firstEntity.setValueList(List.of(solution.getValueList().getFirst())); firstEntity.setPinned(true); var secondEntity = solution.getEntityList().get(1); secondEntity.setValueList(List.of(solution.getValueList().get(1))); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java index 87ebd912e92..2313530f99c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java @@ -3,17 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import java.util.HashMap; - import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; -import ai.timefold.solver.core.impl.score.director.incremental.IncrementalScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -22,53 +18,11 @@ class ScoreDirectorFactoryFactoryTest { - @Test - void incrementalScoreCalculatorWithCustomProperties() { - var config = new ScoreDirectorFactoryConfig(); - config.setIncrementalScoreCalculatorClass( - TestCustomPropertiesIncrementalScoreCalculator.class); - var customProperties = new HashMap(); - customProperties.put("stringProperty", "string 1"); - customProperties.put("intProperty", "7"); - config.setIncrementalScoreCalculatorCustomProperties(customProperties); - - var scoreDirectorFactory = - (IncrementalScoreDirectorFactory) buildTestdataScoreDirectoryFactory(config); - try (var scoreDirector = scoreDirectorFactory.buildScoreDirector()) { - var scoreCalculator = - (TestCustomPropertiesIncrementalScoreCalculator) scoreDirector.getIncrementalScoreCalculator(); - assertThat(scoreCalculator.getStringProperty()).isEqualTo("string 1"); - assertThat(scoreCalculator.getIntProperty()).isEqualTo(7); - } - } - - @Test - void buildWithAssertionScoreDirectorFactory() { - var assertionScoreDirectorConfig = new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(TestCustomPropertiesIncrementalScoreCalculator.class); - var config = new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(TestCustomPropertiesIncrementalScoreCalculator.class) - .withAssertionScoreDirectorFactory(assertionScoreDirectorConfig); - - var scoreDirectorFactory = - (AbstractScoreDirectorFactory) buildTestdataScoreDirectoryFactory(config, - EnvironmentMode.STEP_ASSERT); - - var assertionScoreDirectorFactory = - (IncrementalScoreDirectorFactory) scoreDirectorFactory - .getAssertionScoreDirectorFactory(); - try (var assertionScoreDirector = assertionScoreDirectorFactory.buildScoreDirector()) { - var assertionScoreCalculator = assertionScoreDirector.getIncrementalScoreCalculator(); - assertThat(assertionScoreCalculator).isExactlyInstanceOf(TestCustomPropertiesIncrementalScoreCalculator.class); - } - } - @Test void multipleScoreCalculations_throwsException() { var config = new ScoreDirectorFactoryConfig() .withConstraintProviderClass(TestdataConstraintProvider.class) - .withEasyScoreCalculatorClass(TestCustomPropertiesEasyScoreCalculator.class) - .withIncrementalScoreCalculatorClass(TestCustomPropertiesIncrementalScoreCalculator.class); + .withEasyScoreCalculatorClass(TestCustomPropertiesEasyScoreCalculator.class); assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> buildTestdataScoreDirectoryFactory(config)) .withMessageContaining("scoreDirectorFactory") .withMessageContaining("together"); @@ -125,62 +79,6 @@ public void setIntProperty(int intProperty) { } } - public static class TestCustomPropertiesIncrementalScoreCalculator - implements IncrementalScoreCalculator { - - private String stringProperty; - private int intProperty; - - public String getStringProperty() { - return stringProperty; - } - - public void setStringProperty(String stringProperty) { - this.stringProperty = stringProperty; - } - - public int getIntProperty() { - return intProperty; - } - - public void setIntProperty(int intProperty) { - this.intProperty = intProperty; - } - - @Override - public void resetWorkingSolution(@NonNull TestdataSolution workingSolution) { - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - } - - @Override - public @NonNull SimpleScore calculateScore() { - return SimpleScore.ZERO; - } - } - public static class TestdataConstraintProvider implements ConstraintProvider { @Override public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java deleted file mode 100644 index 2f9cb909ece..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorSemanticsTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package ai.timefold.solver.core.impl.score.director.incremental; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; -import ai.timefold.solver.core.config.solver.EnvironmentMode; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorSemanticsTest; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; -import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; -import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.constraintweightoverrides.TestdataConstraintWeightOverridesSolution; -import ai.timefold.solver.core.testdomain.list.pinned.TestdataPinnedListIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.list.pinned.TestdataPinnedListSolution; -import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListIncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListSolution; - -final class IncrementalScoreDirectorSemanticsTest extends AbstractScoreDirectorSemanticsTest { - - @Override - protected ScoreDirectorFactory - buildScoreDirectorFactoryWithConstraintConfiguration( - SolutionDescriptor solutionDescriptor) { - var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(TestdataConstraintWeightOverridesIncrementalScoreCalculator.class); - var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory( - scoreDirectorFactoryConfig); - return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); - } - - @Override - protected ScoreDirectorFactory - buildScoreDirectorFactoryWithListVariableEntityPin( - SolutionDescriptor solutionDescriptor) { - var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(TestdataPinnedListIncrementalScoreCalculator.class); - var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); - return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); - } - - @Override - protected ScoreDirectorFactory - buildScoreDirectorFactoryWithListVariablePinIndex( - SolutionDescriptor solutionDescriptor) { - var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(TestdataPinnedWithIndexListIncrementalScoreCalculator.class); - var scoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(scoreDirectorFactoryConfig); - return scoreDirectorFactoryFactory.buildScoreDirectorFactory(EnvironmentMode.PHASE_ASSERT, solutionDescriptor); - } - -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java deleted file mode 100644 index 943179969a3..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package ai.timefold.solver.core.impl.score.director.incremental; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; -import ai.timefold.solver.core.impl.score.definition.SimpleScoreDefinition; - -import org.junit.jupiter.api.Test; - -class IncrementalScoreDirectorTest { - - @Test - void illegalStateExceptionThrownWhenConstraintMatchNotEnabled() { - try (var scoreDirector = new IncrementalScoreDirector.Builder<>(mockIncrementalScoreDirectorFactory()) - .withIncrementalScoreCalculator(mockIncrementalScoreCalculator(false)) - .build()) { - scoreDirector.setWorkingSolution(new Object()); - assertThatIllegalStateException() - .isThrownBy(scoreDirector::getConstraintMatchTotalMap) - .withMessageContaining(ConstraintMatchPolicy.DISABLED.name()); - } - } - - @Test - void constraintMatchTotalsNeverNull() { - try (var scoreDirector = new IncrementalScoreDirector.Builder<>(mockIncrementalScoreDirectorFactory()) - .withIncrementalScoreCalculator(mockIncrementalScoreCalculator(true)) - .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) - .build()) { - scoreDirector.setWorkingSolution(new Object()); - assertThat(scoreDirector.getConstraintMatchTotalMap()).isNotNull(); - } - } - - @Test - void constraintMatchIsNotEnabledWhenScoreCalculatorNotConstraintMatchAware() { - try (var scoreDirector = new IncrementalScoreDirector.Builder<>(mockIncrementalScoreDirectorFactory()) - .withIncrementalScoreCalculator(mockIncrementalScoreCalculator(false)) - .withConstraintMatchPolicy(ConstraintMatchPolicy.ENABLED) - .build()) { - assertThat(scoreDirector.getConstraintMatchPolicy()).isEqualTo(ConstraintMatchPolicy.DISABLED); - } - } - - @SuppressWarnings("unchecked") - private IncrementalScoreDirectorFactory mockIncrementalScoreDirectorFactory() { - IncrementalScoreDirectorFactory factory = mock(IncrementalScoreDirectorFactory.class); - when(factory.getScoreDefinition()).thenReturn(new SimpleScoreDefinition()); - SolutionDescriptor solutionDescriptor = mock(SolutionDescriptor.class); - when(factory.getSolutionDescriptor()).thenReturn(solutionDescriptor); - return factory; - } - - @SuppressWarnings("unchecked") - private IncrementalScoreCalculator mockIncrementalScoreCalculator(boolean constraintMatchAware) { - return constraintMatchAware - ? mock(ConstraintMatchAwareIncrementalScoreCalculator.class) - : mock(IncrementalScoreCalculator.class); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java index 727acd3e0f3..589ff330c60 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java @@ -40,7 +40,7 @@ void joinWithNullKeyFromRight() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -101,7 +101,7 @@ void filteringJoinNullConflict() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -154,7 +154,7 @@ void filteringIfExistsNullConflict() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -201,7 +201,7 @@ void filteringIfNotExistsNullConflict() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -263,7 +263,7 @@ void filteringJoinNullConflictDifferentNodes() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -320,7 +320,7 @@ void filteringIfExistsNullConflictDifferentNodes() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -374,7 +374,7 @@ void filteringIfNotExistsNullConflictDifferentNodes() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); @@ -423,7 +423,7 @@ void mapPlanningEntityChanges() { var solution = TestdataSolution.generateSolution(1, 2); var entity1 = solution.getEntityList().get(0); var entity2 = solution.getEntityList().get(1); - var value = solution.getValueList().get(0); + var value = solution.getValueList().getFirst(); entity1.setValue(null); entity2.setValue(value); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java index 24ad9042e46..3da087f0880 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java @@ -12,14 +12,15 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution; @@ -33,6 +34,7 @@ public abstract class AbstractConstraintStreamTest { protected static final String TEST_CONSTRAINT_NAME = "testConstraintName"; + protected static final ConstraintRef TEST_CONSTRAINT_REF = new ConstraintRef(TEST_CONSTRAINT_NAME); protected final ConstraintStreamImplSupport implSupport; @@ -70,7 +72,7 @@ protected void assertScore(InnerScoreDirector void assertScore(InnerScoreDirector assertableMatch.constraintName - .equals(constraintMatch.getConstraintRef().constraintName())) + .filter(assertableMatch -> assertableMatch.constraintRef.equals(constraintMatch.getConstraintRef())) .noneMatch(assertableMatch -> assertableMatch.isEqualTo(constraintMatch))) { fail("The constraintMatch (" + constraintMatch + ") is in excess," + " it's not in the assertableMatches (" + Arrays.toString(assertableMatches) + ")."); @@ -110,26 +111,26 @@ protected static AssertableMatch assertMatch(Object... justifications) { } protected static AssertableMatch assertMatch(String constraintName, Object... justifications) { - return assertMatchWithScore(-1, constraintName, justifications); + return assertMatchWithScore(-1, ConstraintRef.of(constraintName), justifications); } protected static AssertableMatch assertMatchWithScore(int score, Object... justifications) { - return assertMatchWithScore(score, TEST_CONSTRAINT_NAME, justifications); + return assertMatchWithScore(score, TEST_CONSTRAINT_REF, justifications); } - protected static AssertableMatch assertMatchWithScore(int score, String constraintName, Object... justifications) { - return new AssertableMatch(score, constraintName, justifications); + protected static AssertableMatch assertMatchWithScore(int score, ConstraintRef constraintRef, Object... justifications) { + return new AssertableMatch(score, constraintRef, justifications); } protected static class AssertableMatch { private final int score; - private final String constraintName; + private final ConstraintRef constraintRef; private final List justificationList; - public AssertableMatch(int score, String constraintName, Object... justifications) { + public AssertableMatch(int score, ConstraintRef constraintRef, Object... justifications) { this.justificationList = Arrays.asList(justifications); - this.constraintName = constraintName; + this.constraintRef = constraintRef; this.score = score; } @@ -137,7 +138,7 @@ public boolean isEqualTo(ConstraintMatch constraintMatch) { if (score != ((SimpleScore) constraintMatch.getScore()).score()) { return false; } - if (!constraintName.equals(constraintMatch.getConstraintRef().constraintName())) { + if (!constraintRef.equals(constraintMatch.getConstraintRef())) { return false; } var justification = constraintMatch.getJustification(); @@ -153,13 +154,13 @@ public boolean isEqualTo(ConstraintMatch constraintMatch) { Assertions.fail("Expected number of justifications (" + justificationList.size() + ") does not match actual (1; " + justification + ")."); } - return justification == justificationList.get(0); + return justification == justificationList.getFirst(); } } @Override public String toString() { - return constraintName + " " + justificationList + "=" + score; + return constraintRef + " " + justificationList + "=" + score; } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractSolutionManagerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractSolutionManagerTest.java index bb1526f855e..281bbba7419 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractSolutionManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractSolutionManagerTest.java @@ -2,19 +2,15 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; -import java.util.Collections; import java.util.List; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.api.solver.SolutionManagerTest; import ai.timefold.solver.core.api.solver.SolutionUpdatePolicy; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.core.testdomain.TestdataEntity; -import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.list.unassignedvar.pinned.TestdataPinnedUnassignedValuesListEntity; import ai.timefold.solver.core.testdomain.list.unassignedvar.pinned.TestdataPinnedUnassignedValuesListSolution; import ai.timefold.solver.core.testdomain.list.unassignedvar.pinned.TestdataPinnedUnassignedValuesListValue; @@ -27,39 +23,6 @@ public abstract class AbstractSolutionManagerTest { protected abstract ScoreDirectorFactoryConfig buildUnassignedWithPinningScoreDirectorFactoryConfig(); - @Test - void indictmentsPresentOnFreshExplanation() { - // Create the environment. - var scoreDirectorFactoryConfig = buildScoreDirectorFactoryConfig(); - var solverConfig = new SolverConfig(); - solverConfig.setSolutionClass(TestdataSolution.class); - solverConfig.setEntityClassList(Collections.singletonList(TestdataEntity.class)); - solverConfig.setScoreDirectorFactoryConfig(scoreDirectorFactoryConfig); - var solverFactory = SolverFactory. create(solverConfig); - var solutionManager = - SolutionManagerTest.SolutionManagerSource.FROM_SOLVER_FACTORY.createSolutionManager(solverFactory); - - // Prepare the solution. - var entityCount = 3; - var solution = TestdataSolution.generateSolution(2, entityCount); - var scoreExplanation = solutionManager.explain(solution); - - // Check for expected results. - assertSoftly(softly -> { - softly.assertThat(scoreExplanation.getScore()) - .isEqualTo(SimpleScore.of(-entityCount)); - softly.assertThat(scoreExplanation.getConstraintMatchTotalMap()) - .isNotEmpty(); - softly.assertThat(scoreExplanation.getIndictmentMap()) - .isNotEmpty(); - var constraintJustificationList = (List) scoreExplanation.getJustificationList(); - softly.assertThat(constraintJustificationList) - .isNotEmpty(); - softly.assertThat(scoreExplanation.getJustificationList(DefaultConstraintJustification.class)) - .containsExactlyElementsOf(constraintJustificationList); - }); - } - @Test void updateAssignedValueWithNullInverseRelation() { // Create the environment. diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java index 87969c668cc..cc686f110cc 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java @@ -220,5 +220,4 @@ default void flatten() { void failWithMultipleJustifications(); - void failWithMultipleIndictments(); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 79706e6ab1a..7d9795acdf6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -17,21 +17,17 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamFunctionalTest; @@ -66,19 +62,19 @@ protected AbstractBiConstraintStreamTest(ConstraintStreamImplSupport implSupport @Override @TestTemplate public void filter_entity() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value1); + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal(TestdataLavishEntity::getValue, Function.identity())) .filter((entity, value) -> value.getCode().equals("MyValue 1")) @@ -110,14 +106,14 @@ public void filter_entity() { @Override @TestTemplate public void filter_consecutive() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(5, 5); - TestdataLavishEntity entity1 = solution.getEntityList().get(0); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntity entity4 = solution.getEntityList().get(3); - TestdataLavishEntity entity5 = solution.getEntityList().get(4); - - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var solution = TestdataLavishSolution.generateSolution(5, 5); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); + var entity5 = solution.getEntityList().get(4); + + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class, filtering((entityA, entityB) -> !Objects.equals(entityA, entity1))) .filter((entityA, entityB) -> !Objects.equals(entityA, entity2)) @@ -139,7 +135,7 @@ public void filter_consecutive() { @TestTemplate public void join_filterOnAssignedValue_unassignOne() { var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(2, 1); - var entity = solution.getEntityList().get(0); + var entity = solution.getEntityList().getFirst(); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); @@ -206,7 +202,7 @@ public void join_filterOnAssignedValue_unassignOne() { @TestTemplate public void join_filterOnAssignedValue_unassignOneReassignOther() { var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(2, 1); - var entity = solution.getEntityList().get(0); + var entity = solution.getEntityList().getFirst(); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); @@ -230,7 +226,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.setWorkingSolution(solution); scoreDirector.beforeListVariableElementAssigned(entity, "valueList", value1); scoreDirector.beforeListVariableChanged(entity, "valueList", 0, 0); - entity.getValueList().addAll(List.of(value1)); + entity.getValueList().add(value1); scoreDirector.afterListVariableChanged(entity, "valueList", 0, 1); scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); @@ -269,23 +265,23 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { @Override @TestTemplate public void join_0() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); solution.getExtraList().add(extra3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal(TestdataLavishEntity::getValue, Function.identity())) .join(TestdataLavishExtra.class) @@ -321,28 +317,28 @@ public void join_0() { @Override @TestTemplate public void join_1Equal() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); entity1.setStringProperty("MyString"); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); entity2.setStringProperty(null); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); extra1.setStringProperty("MyString"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); extra2.setStringProperty(null); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); extra3.setStringProperty("MyString"); solution.getExtraList().add(extra3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal(TestdataLavishEntity::getValue, Function.identity())) .join(TestdataLavishExtra.class, @@ -374,28 +370,28 @@ public void join_1Equal() { @TestTemplate public void join_1Filtering() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); entity1.setStringProperty("MyString"); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); entity2.setStringProperty(null); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); extra1.setStringProperty("MyString"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); extra2.setStringProperty(null); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); extra3.setStringProperty("MyString"); solution.getExtraList().add(extra3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, filtering((entity, value) -> Objects.equals(entity.getValue(), value))) .join(TestdataLavishExtra.class, @@ -428,33 +424,33 @@ public void join_1Filtering() { @Override @TestTemplate public void join_2Equal() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); entity1.setStringProperty("MyString"); entity1.setIntegerProperty(7); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); entity2.setStringProperty(null); entity2.setIntegerProperty(8); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); extra1.setStringProperty("MyString"); extra1.setIntegerProperty(8); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); extra2.setStringProperty(null); extra2.setIntegerProperty(7); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); extra3.setStringProperty("MyString"); extra3.setIntegerProperty(7); solution.getExtraList().add(extra3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal(TestdataLavishEntity::getValue, Function.identity())) .join(TestdataLavishExtra.class, @@ -495,19 +491,19 @@ public void join_filtering_comesLast() { @TestTemplate public void join_mixedEqualsAndFiltering() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); entity1.setStringProperty("MyString"); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); entity2.setStringProperty(null); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal(TestdataLavishEntity::getValue, Function.identity()), @@ -537,21 +533,21 @@ public void join_mixedEqualsAndFiltering() { @Override @TestTemplate public void joinAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .groupBy(countDistinct(TestdataLavishEntity::getValue), countDistinct(TestdataLavishEntity::getValue)) @@ -597,7 +593,7 @@ public void ifExists_unknownClass() { @TestTemplate public void ifExists_filterOnAssignedValue_unassignOne() { var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(2, 1); - var entity = solution.getEntityList().get(0); + var entity = solution.getEntityList().getFirst(); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); @@ -660,7 +656,7 @@ public void ifExists_filterOnAssignedValue_unassignOne() { @TestTemplate public void ifExists_filterOnAssignedValue_unassignOneReassignOther() { var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(2, 1); - var entity = solution.getEntityList().get(0); + var entity = solution.getEntityList().getFirst(); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); @@ -684,7 +680,7 @@ public void ifExists_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.setWorkingSolution(solution); scoreDirector.beforeListVariableElementAssigned(entity, "valueList", value1); scoreDirector.beforeListVariableChanged(entity, "valueList", 0, 0); - entity.getValueList().addAll(List.of(value1)); + entity.getValueList().add(value1); scoreDirector.afterListVariableChanged(entity, "valueList", 0, 1); scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); @@ -723,11 +719,11 @@ public void ifExists_filterOnAssignedValue_unassignOneReassignOther() { @Override @TestTemplate public void ifExists_0Joiner0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishValueGroup valueGroup = new TestdataLavishValueGroup("MyValueGroup"); + var solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); + var valueGroup = new TestdataLavishValueGroup("MyValueGroup"); solution.getValueGroupList().add(valueGroup); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishValueGroup.class) .ifExists(TestdataLavishEntityGroup.class) .penalize(SimpleScore.ONE) @@ -739,7 +735,7 @@ public void ifExists_0Joiner0Filter() { assertMatch(valueGroup, solution.getFirstValueGroup())); // Incremental - TestdataLavishEntityGroup entityGroup = solution.getFirstEntityGroup(); + var entityGroup = solution.getFirstEntityGroup(); scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); @@ -749,16 +745,16 @@ public void ifExists_0Joiner0Filter() { @Override @TestTemplate public void ifExists_0Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifExists(TestdataLavishEntityGroup.class, filtering((entityA, entityB, group) -> Objects.equals(group, entityA.getEntityGroup()) && @@ -772,7 +768,7 @@ public void ifExists_0Join1Filter() { assertMatch(solution.getFirstEntity(), entity2)); // Incremental - TestdataLavishEntityGroup toRemove = solution.getFirstEntityGroup(); + var toRemove = solution.getFirstEntityGroup(); scoreDirector.beforeProblemFactRemoved(toRemove); solution.getEntityGroupList().remove(toRemove); scoreDirector.afterProblemFactRemoved(toRemove); @@ -782,16 +778,16 @@ public void ifExists_0Join1Filter() { @Override @TestTemplate public void ifExists_1Join0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifExists(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), Function.identity())) @@ -817,16 +813,16 @@ public void ifExists_1Join0Filter() { @Override @TestTemplate public void ifExists_1Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifExists(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), Function.identity()), @@ -850,16 +846,16 @@ public void ifExists_1Join1Filter() { @Override @TestTemplate public void ifExistsDoesNotIncludeUnassigned() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), null); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifExists(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) @@ -885,11 +881,11 @@ public void ifNotExists_unknownClass() { @Override @TestTemplate public void ifNotExists_0Joiner0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishValueGroup valueGroup = new TestdataLavishValueGroup("MyValueGroup"); + var solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); + var valueGroup = new TestdataLavishValueGroup("MyValueGroup"); solution.getValueGroupList().add(valueGroup); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishValueGroup.class) .ifNotExists(TestdataLavishEntityGroup.class) .penalize(SimpleScore.ONE) @@ -900,7 +896,7 @@ public void ifNotExists_0Joiner0Filter() { assertScore(scoreDirector); // Incremental - TestdataLavishEntityGroup entityGroup = solution.getFirstEntityGroup(); + var entityGroup = solution.getFirstEntityGroup(); scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); @@ -911,16 +907,16 @@ public void ifNotExists_0Joiner0Filter() { @Override @TestTemplate public void ifNotExists_0Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifNotExists(TestdataLavishEntityGroup.class, filtering((entityA, entityB, group) -> Objects.equals(group, entityA.getEntityGroup()) && @@ -935,7 +931,7 @@ public void ifNotExists_0Join1Filter() { assertMatch(entity1, entity2)); // Incremental - TestdataLavishEntityGroup toRemove = solution.getFirstEntityGroup(); + var toRemove = solution.getFirstEntityGroup(); scoreDirector.beforeProblemFactRemoved(toRemove); solution.getEntityGroupList().remove(toRemove); scoreDirector.afterProblemFactRemoved(toRemove); @@ -948,16 +944,16 @@ public void ifNotExists_0Join1Filter() { @Override @TestTemplate public void ifNotExists_1Join0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifNotExists(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), Function.identity())) @@ -979,16 +975,16 @@ public void ifNotExists_1Join0Filter() { @Override @TestTemplate public void ifNotExists_1Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifNotExists(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), Function.identity()), @@ -1016,16 +1012,16 @@ public void ifNotExists_1Join1Filter() { @Override @TestTemplate public void ifNotExistsDoesNotIncludeUnassigned() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), null); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .ifNotExists(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) @@ -1048,21 +1044,21 @@ public void ifNotExistsDoesNotIncludeUnassigned() { @Override @TestTemplate public void ifExistsAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .groupBy(countDistinct(TestdataLavishEntity::getValue), countDistinct(TestdataLavishEntity::getValue)) @@ -1093,18 +1089,18 @@ public void ifExistsAfterGroupBy() { @Override @TestTemplate public void groupBy_1Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 7); - TestdataLavishEntityGroup entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 7); + var entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup1); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); + var entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) .groupBy((entityA, entityB) -> entityA.getEntityGroup()) .penalize(SimpleScore.ONE) @@ -1128,9 +1124,9 @@ public void groupBy_1Mapping0Collector() { @Override @TestTemplate public void groupBy_1Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 3, 7); + var solution = TestdataLavishSolution.generateSolution(2, 5, 3, 7); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((entityA, entityB) -> entityA.toString(), countBi()) .filter((entity, count) -> count > 4) @@ -1140,24 +1136,24 @@ public void groupBy_1Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, solution.getFirstEntity().toString(), 6L), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, solution.getEntityList().get(1).toString(), 5L)); + assertMatchWithScore(-1, solution.getFirstEntity().toString(), 6L), + assertMatchWithScore(-1, solution.getEntityList().get(1).toString(), 5L)); // Incremental; we have a new first entity, and less entities in total. - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, solution.getFirstEntity().toString(), 5L)); + assertMatchWithScore(-1, solution.getFirstEntity().toString(), 5L)); } @Override @TestTemplate public void groupBy_1Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((entityA, entityB) -> entityA.toString(), countBi(), @@ -1165,30 +1161,30 @@ public void groupBy_1Mapping2Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); } @Override @TestTemplate public void groupBy_1Mapping3Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((entityA, entityB) -> entityA.toString(), min((TestdataLavishEntity entityA, TestdataLavishEntity entityB) -> entityA.getLongProperty()), @@ -1197,35 +1193,35 @@ public void groupBy_1Mapping3Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(Long.MAX_VALUE); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, + assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); } @Override @TestTemplate public void groupBy_0Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 2, 3); + var solution = TestdataLavishSolution.generateSolution(2, 5, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy(countBi()) .penalize(SimpleScore.ONE, count -> count) @@ -1236,7 +1232,7 @@ public void groupBy_0Mapping1Collector() { assertScore(scoreDirector, assertMatchWithScore(-3, 3L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1246,15 +1242,15 @@ public void groupBy_0Mapping1Collector() { @Override @TestTemplate public void groupBy_0Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy(countBi(), countDistinct((e, e2) -> e)) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1270,8 +1266,8 @@ public void groupBy_0Mapping2Collector() { @Override @TestTemplate public void groupBy_0Mapping3Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy(countBi(), min((TestdataLavishEntity e, TestdataLavishEntity e2) -> e.getLongProperty()), @@ -1279,11 +1275,11 @@ public void groupBy_0Mapping3Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(0L); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(1L); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity3 = solution.getEntityList().get(2); entity3.setLongProperty(2L); // From scratch @@ -1302,8 +1298,8 @@ public void groupBy_0Mapping3Collector() { @Override @TestTemplate public void groupBy_0Mapping4Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy(countBi(), min((TestdataLavishEntity e, TestdataLavishEntity e2) -> e.getLongProperty()), @@ -1312,11 +1308,11 @@ public void groupBy_0Mapping4Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(0L); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(1L); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity3 = solution.getEntityList().get(2); entity3.setLongProperty(2L); // From scratch @@ -1335,16 +1331,16 @@ public void groupBy_0Mapping4Collector() { @Override @TestTemplate public void groupBy_2Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 5, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1354,7 +1350,7 @@ public void groupBy_2Mapping0Collector() { assertMatchWithScore(-1, group2, group3)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1365,16 +1361,16 @@ public void groupBy_2Mapping0Collector() { @Override @TestTemplate public void groupBy_2Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 4); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 4); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), countBi()) .penalize(SimpleScore.ONE, (entityGroup1, entityGroup2, count) -> count) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1385,7 +1381,7 @@ public void groupBy_2Mapping1Collector() { assertMatchWithScore(-1, group2, group2, 1L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1398,17 +1394,17 @@ public void groupBy_2Mapping1Collector() { @Override @TestTemplate public void groupBy_2Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 4); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 4); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), countBi(), countBi()) .penalize(SimpleScore.ONE, (entityGroup1, entityGroup2, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1419,7 +1415,7 @@ public void groupBy_2Mapping2Collector() { assertMatchWithScore(-2, group2, group2, 1L, 1L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1432,18 +1428,18 @@ public void groupBy_2Mapping2Collector() { @Override @TestTemplate public void groupBy_3Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), (a, b) -> a.getValue()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); - TestdataLavishValue value1 = solution.getValueList().get(0); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); + var value1 = solution.getValueList().get(0); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1453,7 +1449,7 @@ public void groupBy_3Mapping0Collector() { assertMatchWithScore(-1, group2, group3, value2)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1464,19 +1460,19 @@ public void groupBy_3Mapping0Collector() { @Override @TestTemplate public void groupBy_3Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), (a, b) -> a.getValue(), ConstraintCollectors.countBi()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); - TestdataLavishValue value1 = solution.getValueList().get(0); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); + var value1 = solution.getValueList().get(0); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1486,7 +1482,7 @@ public void groupBy_3Mapping1Collector() { assertMatchWithScore(-1, group2, group3, value2, 1L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1497,19 +1493,19 @@ public void groupBy_3Mapping1Collector() { @Override @TestTemplate public void groupBy_4Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), (a, b) -> a.getValue(), (a, b) -> b.getValue()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); - TestdataLavishValue value1 = solution.getValueList().get(0); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); + var value1 = solution.getValueList().get(0); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1519,7 +1515,7 @@ public void groupBy_4Mapping0Collector() { assertMatchWithScore(-1, group2, group3, value2, value1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1530,16 +1526,16 @@ public void groupBy_4Mapping0Collector() { @Override @TestTemplate public void distinct() { // On a distinct stream, this is a no-op. - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .distinct() .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1552,15 +1548,15 @@ public void distinct() { // On a distinct stream, this is a no-op. @Override @TestTemplate public void mapToUniWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> asSet(a.getEntityGroup(), b.getEntityGroup())) // 3 entities, 2 groups => duplicates. .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1569,7 +1565,7 @@ public void mapToUniWithDuplicates() { assertMatch(asSet(group1, group2)), assertMatch(asSet(group1))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1582,16 +1578,16 @@ public void mapToUniWithDuplicates() { @Override @TestTemplate public void mapToUniWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> asSet(a.getEntityGroup(), b.getEntityGroup())) // 3 entities, 3 groups => no duplicates. .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1600,7 +1596,7 @@ public void mapToUniWithoutDuplicates() { assertMatch(asSet(group1, group3)), assertMatch(asSet(group2, group3))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1613,16 +1609,16 @@ public void mapToUniWithoutDuplicates() { @Override @TestTemplate public void mapToUniAndDistinctWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> asSet(a.getEntityGroup(), b.getEntityGroup())) // 3 entities, 2 groups => duplicates. .distinct() // Duplicate copies removed here. .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1630,7 +1626,7 @@ public void mapToUniAndDistinctWithDuplicates() { assertMatch(asSet(group1, group2)), assertMatch(asSet(group1))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1643,17 +1639,17 @@ public void mapToUniAndDistinctWithDuplicates() { @Override @TestTemplate public void mapToUniAndDistinctWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> asSet(a.getEntityGroup(), b.getEntityGroup())) // 3 entities, 3 groups => no duplicates. .distinct() .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1662,7 +1658,7 @@ public void mapToUniAndDistinctWithoutDuplicates() { assertMatch(asSet(group1, group3)), assertMatch(asSet(group2, group3))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1675,16 +1671,16 @@ public void mapToUniAndDistinctWithoutDuplicates() { @Override @TestTemplate public void mapToBi() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1693,7 +1689,7 @@ public void mapToBi() { assertMatch(group2, group1), assertMatch(group1, group1)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1706,8 +1702,8 @@ public void mapToBi() { @Override @TestTemplate public void mapToTri() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), @@ -1715,11 +1711,11 @@ public void mapToTri() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - long sum01 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty(); - long sum02 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(2).getLongProperty(); - long sum12 = solution.getEntityList().get(1).getLongProperty() + solution.getEntityList().get(2).getLongProperty(); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var sum01 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty(); + var sum02 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(2).getLongProperty(); + var sum12 = solution.getEntityList().get(1).getLongProperty() + solution.getEntityList().get(2).getLongProperty(); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1728,7 +1724,7 @@ public void mapToTri() { assertMatch(group2, group1, sum12), assertMatch(group1, group1, sum02)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1741,8 +1737,8 @@ public void mapToTri() { @Override @TestTemplate public void mapToQuad() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .map((a, b) -> a.getEntityGroup(), (a, b) -> b.getEntityGroup(), @@ -1751,10 +1747,10 @@ public void mapToQuad() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var value1 = solution.getFirstValue(); + var group2 = solution.getEntityGroupList().get(1); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1763,7 +1759,7 @@ public void mapToQuad() { assertMatch(group2, group1, value2, value1), assertMatch(group1, group1, value1, value1)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1776,16 +1772,16 @@ public void mapToQuad() { @Override @TestTemplate public void expandToTri() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .expand((a, b) -> a.getLongProperty() + b.getLongProperty()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - long sum01 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty(); - long sum02 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(2).getLongProperty(); - long sum12 = solution.getEntityList().get(1).getLongProperty() + solution.getEntityList().get(2).getLongProperty(); + var sum01 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty(); + var sum02 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(2).getLongProperty(); + var sum12 = solution.getEntityList().get(1).getLongProperty() + solution.getEntityList().get(2).getLongProperty(); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1794,7 +1790,7 @@ public void expandToTri() { assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12), assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1807,22 +1803,22 @@ public void expandToTri() { @Override @TestTemplate public void expandToQuad() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .expand((a, b) -> a.getLongProperty() + b.getLongProperty(), (a, b) -> a.getEntityGroup().getCode() + b.getEntityGroup().getCode()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - long sum01 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty(); - long sum02 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(2).getLongProperty(); - long sum12 = solution.getEntityList().get(1).getLongProperty() + solution.getEntityList().get(2).getLongProperty(); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - String concat01 = group1.getCode() + group2.getCode(); - String concat02 = group1.getCode() + group1.getCode(); - String concat12 = group2.getCode() + group1.getCode(); + var sum01 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty(); + var sum02 = solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(2).getLongProperty(); + var sum12 = solution.getEntityList().get(1).getLongProperty() + solution.getEntityList().get(2).getLongProperty(); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var concat01 = group1.getCode() + group2.getCode(); + var concat02 = group1.getCode() + group1.getCode(); + var concat12 = group2.getCode() + group1.getCode(); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1831,7 +1827,7 @@ public void expandToQuad() { assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12, concat12), assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02, concat02)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1844,13 +1840,13 @@ public void expandToQuad() { @Override @TestTemplate public void flatten() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .flatten((a, b) -> asList(a.getEntityGroup(), b.getEntityGroup(), group2)) .penalize(SimpleScore.ONE) @@ -1873,13 +1869,13 @@ public void flatten() { @Override @TestTemplate public void flattenLastWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .flattenLast(b -> asList(b.getEntityGroup(), group1, group2)) .penalize(SimpleScore.ONE) @@ -1911,13 +1907,13 @@ public void flattenLastWithDuplicates() { @Override @TestTemplate public void flattenLastWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .flattenLast(b -> singleton(b.getEntityGroup())) .penalize(SimpleScore.ONE) @@ -1941,13 +1937,13 @@ public void flattenLastWithoutDuplicates() { @Override @TestTemplate public void flattenLastAndDistinctWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .flattenLast(b -> asList(b.getEntityGroup(), group1, group2)) .distinct() @@ -1974,13 +1970,13 @@ public void flattenLastAndDistinctWithDuplicates() { @Override @TestTemplate public void flattenLastAndDistinctWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .flattenLast(b -> singleton(b.getEntityGroup())) .distinct() @@ -2005,21 +2001,21 @@ public void flattenLastAndDistinctWithoutDuplicates() { @Override @TestTemplate public void concatUniWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2051,21 +2047,21 @@ public void concatUniWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctUniWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2098,21 +2094,21 @@ public void concatAndDistinctUniWithoutValueDuplicates() { @Override @TestTemplate public void concatBiWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2146,21 +2142,21 @@ public void concatBiWithoutValueDuplicates() { @Override @TestTemplate public void concatBiWithValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2194,21 +2190,21 @@ public void concatBiWithValueDuplicates() { @Override @TestTemplate public void concatAndDistinctBiWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2243,21 +2239,21 @@ public void concatAndDistinctBiWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctBiWithValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2290,21 +2286,21 @@ public void concatAndDistinctBiWithValueDuplicates() { @Override @TestTemplate public void concatTriWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2340,21 +2336,21 @@ public void concatTriWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctTriWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2391,21 +2387,21 @@ public void concatAndDistinctTriWithoutValueDuplicates() { @Override @TestTemplate public void concatQuadWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2443,21 +2439,21 @@ public void concatQuadWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctQuadWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2496,21 +2492,21 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { @Override @TestTemplate public void concatAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntity.class) .filter((e1, e2) -> e1.getValue() == value1 && e2.getValue() == value2) @@ -2598,9 +2594,9 @@ public void complement() { @Override @TestTemplate public void penalizeUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2613,7 +2609,7 @@ public void penalizeUnweighted() { @Override @TestTemplate public void penalizeUnweightedBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2632,28 +2628,22 @@ private , Solution_, Entity_> void assertDefaultJus if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList); - - Map> constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); - assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + assertThat(constraintMatchTotalMap).containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(entityList.size() * 3); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); - for (int i = 0; i < entityList.size(); i++) { - ConstraintMatch constraintMatch = constraintMatchList.get(i); + for (var i = 0; i < entityList.size(); i++) { + var constraintMatch = constraintMatchList.get(i); assertSoftly(softly -> { - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(DefaultConstraintJustification.class); - DefaultConstraintJustification castJustification = + var castJustification = (DefaultConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .hasSize(2); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .hasSize(2); }); } } @@ -2661,9 +2651,9 @@ private , Solution_, Entity_> void assertDefaultJus @Override @TestTemplate public void penalize() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .penalize(SimpleScore.ONE, (entity, entity2) -> 2) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2676,7 +2666,7 @@ public void penalize() { @Override @TestTemplate public void penalizeBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2695,9 +2685,9 @@ public void penalizeBigDecimal() { @Override @TestTemplate public void rewardUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .reward(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2710,9 +2700,9 @@ public void rewardUnweighted() { @Override @TestTemplate public void reward() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .reward(SimpleScore.ONE, (entity, entity2) -> 2) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2725,7 +2715,7 @@ public void reward() { @Override @TestTemplate public void rewardBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2744,9 +2734,9 @@ public void rewardBigDecimal() { @Override @TestTemplate public void impactPositiveUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .impact(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2759,9 +2749,9 @@ public void impactPositiveUnweighted() { @Override @TestTemplate public void impactPositive() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .impact(SimpleScore.ONE, (entity, entity2) -> 2) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2774,7 +2764,7 @@ public void impactPositive() { @Override @TestTemplate public void impactPositiveBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2793,9 +2783,9 @@ public void impactPositiveBigDecimal() { @Override @TestTemplate public void impactNegative() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .impact(SimpleScore.ONE, (entity, entity2) -> -2) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -2808,7 +2798,7 @@ public void impactNegative() { @Override @TestTemplate public void impactNegativeBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2827,13 +2817,12 @@ public void impactNegativeBigDecimal() { @Override @TestTemplate public void penalizeUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .penalize(SimpleScore.ONE) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -2846,28 +2835,23 @@ private , Solution_, Entity_> void assertCustomJust if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList); - - Map> constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + .containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(entityList.size() * 3); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); - for (int i = 0; i < entityList.size(); i++) { - ConstraintMatch constraintMatch = constraintMatchList.get(i); + for (var i = 0; i < entityList.size(); i++) { + var constraintMatch = constraintMatchList.get(i); assertSoftly(softly -> { - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(TestConstraintJustification.class); - TestConstraintJustification castJustification = + var castJustification = (TestConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .hasSize(2); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .hasSize(2); }); } } @@ -2875,13 +2859,12 @@ private , Solution_, Entity_> void assertCustomJust @Override @TestTemplate public void penalizeCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .penalize(SimpleScore.ONE, (entity, entity2) -> 2) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -2892,7 +2875,7 @@ public void penalizeCustomJustifications() { @Override @TestTemplate public void penalizeBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2901,7 +2884,6 @@ public void penalizeBigDecimalCustomJustifications() { .penalizeBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2) -> BigDecimal.valueOf(2)) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -2913,13 +2895,12 @@ public void penalizeBigDecimalCustomJustifications() { @Override @TestTemplate public void rewardUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .reward(SimpleScore.ONE) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -2930,13 +2911,12 @@ public void rewardUnweightedCustomJustifications() { @Override @TestTemplate public void rewardCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .reward(SimpleScore.ONE, (entity, entity2) -> 2) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -2947,7 +2927,7 @@ public void rewardCustomJustifications() { @Override @TestTemplate public void rewardBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2956,7 +2936,6 @@ public void rewardBigDecimalCustomJustifications() { .rewardBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2) -> BigDecimal.valueOf(2)) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -2968,13 +2947,12 @@ public void rewardBigDecimalCustomJustifications() { @Override @TestTemplate public void impactPositiveUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .impact(SimpleScore.ONE) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -2985,13 +2963,12 @@ public void impactPositiveUnweightedCustomJustifications() { @Override @TestTemplate public void impactPositiveCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .impact(SimpleScore.ONE, (entity, entity2) -> 2) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3002,7 +2979,7 @@ public void impactPositiveCustomJustifications() { @Override @TestTemplate public void impactPositiveBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -3011,7 +2988,6 @@ public void impactPositiveBigDecimalCustomJustifications() { .impactBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2) -> BigDecimal.valueOf(2)) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -3023,13 +2999,12 @@ public void impactPositiveBigDecimalCustomJustifications() { @Override @TestTemplate public void impactNegativeCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .impact(SimpleScore.ONE, (entity, entity2) -> -2) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3040,7 +3015,7 @@ public void impactNegativeCustomJustifications() { @Override @TestTemplate public void impactNegativeBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -3049,7 +3024,6 @@ public void impactNegativeBigDecimalCustomJustifications() { .impactBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2) -> BigDecimal.valueOf(-2)) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -3066,42 +3040,28 @@ public void failWithMultipleJustifications() { .penalize(SimpleScore.ONE, (entity, entity2) -> 2) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME))) .hasMessageContaining("Maybe the constraint calls justifyWith() twice?"); } - @Override - @TestTemplate - public void failWithMultipleIndictments() { - assertThatCode(() -> buildScoreDirector( - factory -> factory.forEachUniquePair(TestdataLavishEntity.class) - .penalize(SimpleScore.ONE, (entity, entity2) -> 2) - .justifyWith((a, b, score) -> new TestConstraintJustification<>(score, a, b)) - .indictWith(Set::of) - .indictWith(Set::of) - .asConstraint(TEST_CONSTRAINT_NAME))) - .hasMessageContaining("Maybe the constraint calls indictWith() twice?"); - } - @TestTemplate public void joinerEqualsAndSameness() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 1, 2); + var solution = TestdataLavishSolution.generateSolution(1, 2, 1, 2); // The two bigDecimals are not the same, but they equals() - String decimal = "0.01"; - BigDecimal bigDecimal1 = new BigDecimal(decimal); - BigDecimal bigDecimal2 = new BigDecimal(decimal); - TestdataLavishEntity entity1 = solution.getEntityList().get(0); + var decimal = "0.01"; + var bigDecimal1 = new BigDecimal(decimal); + var bigDecimal2 = new BigDecimal(decimal); + var entity1 = solution.getEntityList().get(0); entity1.setBigDecimalProperty(bigDecimal1); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setBigDecimalProperty(bigDecimal2); // Entity 3's BigDecimal property is the same as Entity 1's and equals() Entity 2's. - TestdataLavishEntity entity3 = new TestdataLavishEntity("My Entity 0", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("My Entity 0", solution.getFirstEntityGroup(), entity1.getValue()); entity3.setBigDecimalProperty(bigDecimal1); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal(TestdataLavishEntity::getBigDecimalProperty)) .penalize(SimpleScore.ONE) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderTest.java index ddd1ec723cf..c8826aaee44 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/BiConstraintBuilderTest.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.bi; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.stream.AbstractConstraintBuilderTest; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; @@ -19,10 +19,9 @@ class BiConstraintBuilderTest extends AbstractConstraintBuilderTest { protected AbstractConstraintBuilder of(String constraintName, String constraintGroup) { return new BiConstraintBuilderImpl<>( (constraintName1, constraintDescription, constraintGroup1, constraintWeight, impactType, - objectSimpleScoreObjectBiFunction, - objectCollectionFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, + objectSimpleScoreObjectBiFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, ConstraintRef.of(constraintName1), constraintDescription, constraintGroup1, constraintWeight, - impactType, objectSimpleScoreObjectBiFunction, objectCollectionFunction, null), + impactType, objectSimpleScoreObjectBiFunction, null), ScoreImpactType.PENALTY, SimpleScore.ONE); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java index 9a2c54e043b..9eeeddc1cf4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java @@ -18,20 +18,17 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.function.Function; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamFunctionalTest; @@ -60,23 +57,23 @@ protected AbstractQuadConstraintStreamTest(ConstraintStreamImplSupport implSuppo @Override @TestTemplate public void filter_entity() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value1); + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity3); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal((e1, e2) -> e1.getValue(), identity())) .join(TestdataLavishExtra.class) @@ -112,14 +109,14 @@ public void filter_entity() { @Override @TestTemplate public void filter_consecutive() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(5, 5); - TestdataLavishEntity entity1 = solution.getEntityList().get(0); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntity entity4 = solution.getEntityList().get(3); - TestdataLavishEntity entity5 = solution.getEntityList().get(4); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(5, 5); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); + var entity5 = solution.getEntityList().get(4); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((a, b) -> a, identity())) .join(TestdataLavishEntity.class, @@ -161,9 +158,9 @@ public void ifExists_unknownClass() { @Override @TestTemplate public void ifExists_0Joiner0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -174,7 +171,7 @@ public void ifExists_0Joiner0Filter() { .asConstraint(TEST_CONSTRAINT_NAME)); // From scratch - TestdataLavishValueGroup valueGroup = solution.getFirstValueGroup(); + var valueGroup = solution.getFirstValueGroup(); scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue(), @@ -190,16 +187,16 @@ public void ifExists_0Joiner0Filter() { @Override @TestTemplate public void ifExists_0Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 2, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getValueList().get(1)); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), identity())) .join(TestdataLavishValue.class, equal((entityA, entityB, group) -> entityA.getValue(), identity())) @@ -217,7 +214,7 @@ public void ifExists_0Join1Filter() { assertMatch(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue())); // Incremental - TestdataLavishValueGroup toRemove = solution.getFirstValueGroup(); + var toRemove = solution.getFirstValueGroup(); scoreDirector.beforeProblemFactRemoved(toRemove); solution.getValueGroupList().remove(toRemove); scoreDirector.afterProblemFactRemoved(toRemove); @@ -227,16 +224,16 @@ public void ifExists_0Join1Filter() { @Override @TestTemplate public void ifExists_1Join0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), identity())) .join(TestdataLavishValue.class, equal((entityA, entityB, group) -> entityA.getValue(), identity())) @@ -264,16 +261,16 @@ public void ifExists_1Join0Filter() { @Override @TestTemplate public void ifExists_1Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), identity())) .join(TestdataLavishValue.class, equal((entityA, entityB, group) -> entityA.getValue(), identity())) @@ -323,9 +320,9 @@ public void ifNotExists_unknownClass() { @Override @TestTemplate public void ifNotExists_0Joiner0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); + var solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -336,7 +333,7 @@ public void ifNotExists_0Joiner0Filter() { .asConstraint(TEST_CONSTRAINT_NAME)); // From scratch - TestdataLavishValueGroup valueGroup = solution.getFirstValueGroup(); + var valueGroup = solution.getFirstValueGroup(); scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector); @@ -352,16 +349,16 @@ public void ifNotExists_0Joiner0Filter() { @Override @TestTemplate public void ifNotExists_0Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 2, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getValueList().get(1)); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), identity())) .join(TestdataLavishValue.class, equal((entityA, entityB, group) -> entityA.getValue(), identity())) @@ -376,7 +373,7 @@ public void ifNotExists_0Join1Filter() { assertScore(scoreDirector); // Incremental - TestdataLavishValueGroup toRemove = solution.getFirstValueGroup(); + var toRemove = solution.getFirstValueGroup(); scoreDirector.beforeProblemFactRemoved(toRemove); solution.getValueGroupList().remove(toRemove); scoreDirector.afterProblemFactRemoved(toRemove); @@ -389,16 +386,16 @@ public void ifNotExists_0Join1Filter() { @Override @TestTemplate public void ifNotExists_1Join0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), identity())) .join(TestdataLavishValue.class, equal((entityA, entityB, group) -> entityA.getValue(), identity())) @@ -422,16 +419,16 @@ public void ifNotExists_1Join0Filter() { @Override @TestTemplate public void ifNotExists_1Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal((entityA, entityB) -> entityA.getEntityGroup(), identity())) .join(TestdataLavishValue.class, equal((entityA, entityB, group) -> entityA.getValue(), identity())) @@ -465,21 +462,21 @@ public void ifNotExistsDoesNotIncludeUnassigned() { @Override @TestTemplate public void ifExistsAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .groupBy(countDistinct(TestdataLavishEntity::getValue), countDistinct(TestdataLavishEntity::getValue), @@ -517,9 +514,9 @@ public void groupBy_0Mapping1Collector() { * E2 has G2 and V2 * E3 has G1 and V1 */ - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -535,7 +532,7 @@ public void groupBy_0Mapping1Collector() { assertMatchWithScore(-5, 5L)); // E1 G1 V1 E1, E1 G1 V1 E3, E2 G2 V2 E2, E3 G1 V1 E1, E3 G1 V1 E3 // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -546,8 +543,8 @@ public void groupBy_0Mapping1Collector() { @Override @TestTemplate public void groupBy_0Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((e1, e2) -> e1, Function.identity())) .join(TestdataLavishEntity.class, equal((e1, e2, e3) -> e2, Function.identity())) @@ -556,7 +553,7 @@ public void groupBy_0Mapping2Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); // From scratch scoreDirector.setWorkingSolution(solution); @@ -572,8 +569,8 @@ public void groupBy_0Mapping2Collector() { @Override @TestTemplate public void groupBy_0Mapping3Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((e1, e2) -> e1, Function.identity())) .join(TestdataLavishEntity.class, equal((e1, e2, e3) -> e2, Function.identity())) @@ -585,11 +582,11 @@ public void groupBy_0Mapping3Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(0L); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(1L); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity3 = solution.getEntityList().get(2); entity3.setLongProperty(2L); // From scratch @@ -608,8 +605,8 @@ public void groupBy_0Mapping3Collector() { @Override @TestTemplate public void groupBy_0Mapping4Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((e1, e2) -> e1, Function.identity())) .join(TestdataLavishEntity.class, equal((e1, e2, e3) -> e2, Function.identity())) @@ -622,11 +619,11 @@ public void groupBy_0Mapping4Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(0L); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(1L); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity3 = solution.getEntityList().get(2); entity3.setLongProperty(2L); // From scratch @@ -645,9 +642,9 @@ public void groupBy_0Mapping4Collector() { @Override @TestTemplate public void groupBy_1Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -657,8 +654,8 @@ public void groupBy_1Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, @@ -669,9 +666,9 @@ public void groupBy_1Mapping0Collector() { @Override @TestTemplate public void groupBy_1Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -681,8 +678,8 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE, (group, count) -> count) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -691,7 +688,7 @@ public void groupBy_1Mapping1Collector() { assertMatchWithScore(-4, value1, 4L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -703,9 +700,9 @@ public void groupBy_1Mapping1Collector() { @Override @TestTemplate public void groupBy_1Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((entityA, entityB) -> entityA, Function.identity())) .join(TestdataLavishEntity.class, equal((entityA, entityB, entityC) -> entityB, Function.identity())) @@ -715,30 +712,30 @@ public void groupBy_1Mapping2Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); } @Override @TestTemplate public void groupBy_1Mapping3Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((entityA, entityB) -> entityA, Function.identity())) .join(TestdataLavishEntity.class, equal((entityA, entityB, entityC) -> entityB, Function.identity())) @@ -751,34 +748,34 @@ public void groupBy_1Mapping3Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(Long.MAX_VALUE); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, + assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); } @Override @TestTemplate public void groupBy_2Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -788,10 +785,10 @@ public void groupBy_2Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -800,7 +797,7 @@ public void groupBy_2Mapping0Collector() { assertMatchWithScore(-1, group1, value1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -812,9 +809,9 @@ public void groupBy_2Mapping0Collector() { @Override @TestTemplate public void groupBy_2Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -825,10 +822,10 @@ public void groupBy_2Mapping1Collector() { .penalize(SimpleScore.ONE, (group, value, count) -> count) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -837,7 +834,7 @@ public void groupBy_2Mapping1Collector() { assertMatchWithScore(-4, group1, value1, 4L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -849,9 +846,9 @@ public void groupBy_2Mapping1Collector() { @Override @TestTemplate public void groupBy_2Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -862,10 +859,10 @@ public void groupBy_2Mapping2Collector() { .penalize(SimpleScore.ONE, (group, value, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -874,7 +871,7 @@ public void groupBy_2Mapping2Collector() { assertMatchWithScore(-8, group1, value1, 4L, 4L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -886,9 +883,9 @@ public void groupBy_2Mapping2Collector() { @Override @TestTemplate public void groupBy_3Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -900,10 +897,10 @@ public void groupBy_3Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -912,7 +909,7 @@ public void groupBy_3Mapping0Collector() { assertMatchWithScore(-1, group1, group1, value1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -924,9 +921,9 @@ public void groupBy_3Mapping0Collector() { @Override @TestTemplate public void groupBy_3Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -938,8 +935,8 @@ public void groupBy_3Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -948,7 +945,7 @@ public void groupBy_3Mapping1Collector() { assertMatchWithScore(-1, group1, group1, group1, 4L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -960,9 +957,9 @@ public void groupBy_3Mapping1Collector() { @Override @TestTemplate public void groupBy_4Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -975,10 +972,10 @@ public void groupBy_4Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -987,7 +984,7 @@ public void groupBy_4Mapping0Collector() { assertMatchWithScore(-1, group1, group1, group1, value1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -999,8 +996,8 @@ public void groupBy_4Mapping0Collector() { @Override @TestTemplate public void distinct() { // On a distinct stream, this is a no-op. - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1010,11 +1007,11 @@ public void distinct() { // On a distinct stream, this is a no-op. .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1027,8 +1024,8 @@ public void distinct() { // On a distinct stream, this is a no-op. @Override @TestTemplate public void mapToUniWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1038,9 +1035,9 @@ public void mapToUniWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var entity = solution.getFirstEntity(); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1060,8 +1057,8 @@ public void mapToUniWithDuplicates() { @Override @TestTemplate public void mapToUniWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1071,15 +1068,15 @@ public void mapToUniWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(asSet(group1, group2))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1091,8 +1088,8 @@ public void mapToUniWithoutDuplicates() { @Override @TestTemplate public void mapToUniAndDistinctWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1103,9 +1100,9 @@ public void mapToUniAndDistinctWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var entity = solution.getFirstEntity(); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1124,8 +1121,8 @@ public void mapToUniAndDistinctWithDuplicates() { @Override @TestTemplate public void mapToUniAndDistinctWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1136,15 +1133,15 @@ public void mapToUniAndDistinctWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(asSet(group1, group2))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1156,8 +1153,8 @@ public void mapToUniAndDistinctWithoutDuplicates() { @Override @TestTemplate public void mapToBi() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1173,7 +1170,7 @@ public void mapToBi() { assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1185,8 +1182,8 @@ public void mapToBi() { @Override @TestTemplate public void mapToTri() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1204,7 +1201,7 @@ public void mapToTri() { assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty())); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1216,8 +1213,8 @@ public void mapToTri() { @Override @TestTemplate public void mapToQuad() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, Joiners.equal((entity1, entity2) -> entity1.getEntityGroup(), Function.identity())) @@ -1233,11 +1230,11 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(TEST_CONSTRAINT_NAME, solution.getFirstEntity().getCode(), + assertMatchWithScore(-1, solution.getFirstEntity().getCode(), solution.getEntityList().get(1).getCode(), solution.getFirstEntityGroup().getCode(), solution.getEntityGroupList().get(1).getCode())); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1249,14 +1246,14 @@ public void mapToQuad() { @Override @TestTemplate public void flattenLastWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) @@ -1287,14 +1284,14 @@ public void flattenLastWithDuplicates() { @Override @TestTemplate public void flattenLastWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) @@ -1322,14 +1319,14 @@ public void flattenLastWithoutDuplicates() { @Override @TestTemplate public void flattenLastAndDistinctWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) @@ -1358,14 +1355,14 @@ public void flattenLastAndDistinctWithDuplicates() { @Override @TestTemplate public void flattenLastAndDistinctWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) @@ -1394,21 +1391,21 @@ public void flattenLastAndDistinctWithoutDuplicates() { @Override @TestTemplate public void concatUniWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1444,21 +1441,21 @@ public void concatUniWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctUniWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1495,21 +1492,21 @@ public void concatAndDistinctUniWithoutValueDuplicates() { @Override @TestTemplate public void concatBiWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1547,21 +1544,21 @@ public void concatBiWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctBiWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1600,21 +1597,21 @@ public void concatAndDistinctBiWithoutValueDuplicates() { @Override @TestTemplate public void concatTriWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1654,21 +1651,21 @@ public void concatTriWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctTriWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1709,21 +1706,21 @@ public void concatAndDistinctTriWithoutValueDuplicates() { @Override @TestTemplate public void concatQuadWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1765,21 +1762,21 @@ public void concatQuadWithoutValueDuplicates() { @Override @TestTemplate public void concatQuadWithValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1821,21 +1818,21 @@ public void concatQuadWithValueDuplicates() { @Override @TestTemplate public void concatAndDistinctQuadWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1878,21 +1875,21 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctQuadWithValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1933,21 +1930,21 @@ public void concatAndDistinctQuadWithValueDuplicates() { @Override @TestTemplate public void concatAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntity.class) .join(TestdataLavishEntity.class) @@ -2044,9 +2041,9 @@ public void complement() { @Override @TestTemplate public void penalizeUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2055,13 +2052,13 @@ public void penalizeUnweighted() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-2)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void penalizeUnweightedBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2075,41 +2072,31 @@ public void penalizeUnweightedBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-2))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } - private , Solution_, Entity_, Value_> void assertDefaultJustifications( - InnerScoreDirector scoreDirector, List entityList, List valueList) { + private , Solution_> void + assertDefaultJustifications(InnerScoreDirector scoreDirector) { if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList.get(0), - entityList.get(1), - entityList.get(5), - entityList.get(6), - valueList.get(0), - valueList.get(1)); - - Map> constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + .containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(2); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); - for (int i = 0; i < 2; i++) { - ConstraintMatch constraintMatch = constraintMatchList.get(i); + for (var i = 0; i < 2; i++) { + var constraintMatch = constraintMatchList.get(i); assertSoftly(softly -> { - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(DefaultConstraintJustification.class); - DefaultConstraintJustification castJustification = + var castJustification = (DefaultConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .hasSize(4); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .hasSize(4); }); } } @@ -2117,9 +2104,9 @@ private , Solution_, Entity_, Value_> void assertDe @Override @TestTemplate public void penalize() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2128,13 +2115,13 @@ public void penalize() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void penalizeBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2149,15 +2136,15 @@ public void penalizeBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void rewardUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2166,15 +2153,15 @@ public void rewardUnweighted() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void reward() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2183,13 +2170,13 @@ public void reward() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void rewardBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2205,15 +2192,15 @@ public void rewardBigDecimal() { scoreDirector.setWorkingSolution(solution); scoreDirector.calculateScore(); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2222,15 +2209,15 @@ public void impactPositiveUnweighted() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactPositive() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2239,13 +2226,13 @@ public void impactPositive() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2260,15 +2247,15 @@ public void impactPositiveBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactNegative() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) @@ -2277,13 +2264,13 @@ public void impactNegative() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactNegativeBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2298,60 +2285,49 @@ public void impactNegativeBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void penalizeUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .penalize(SimpleScore.ONE) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-2)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } - private , Solution_, Entity_, Value_> void assertCustomJustifications( - InnerScoreDirector scoreDirector, List entityList, List valueList) { + private , Solution_> void + assertCustomJustifications(InnerScoreDirector scoreDirector) { if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList.get(0), - entityList.get(1), - entityList.get(5), - entityList.get(6), - valueList.get(0), - valueList.get(1)); - - Map> constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + .containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(2); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); - for (int i = 0; i < 2; i++) { - ConstraintMatch constraintMatch = constraintMatchList.get(i); + for (var i = 0; i < 2; i++) { + var constraintMatch = constraintMatchList.get(i); assertSoftly(softly -> { - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(TestConstraintJustification.class); - TestConstraintJustification castJustification = + var castJustification = (TestConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .hasSize(4); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .hasSize(4); }); } } @@ -2359,26 +2335,25 @@ private , Solution_, Entity_, Value_> void assertCu @Override @TestTemplate public void penalizeCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .penalize(SimpleScore.ONE, (entity, entity2, value, value2) -> 2) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void penalizeBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2390,57 +2365,54 @@ public void penalizeBigDecimalCustomJustifications() { (entity, entity2, value, value2) -> BigDecimal.valueOf(2)) .justifyWith( (a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void rewardUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .reward(SimpleScore.ONE) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void rewardCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .reward(SimpleScore.ONE, (entity, entity2, value, value2) -> 2) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void rewardBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2452,57 +2424,54 @@ public void rewardBigDecimalCustomJustifications() { (entity, entity2, value, value2) -> BigDecimal.valueOf(2)) .justifyWith( (a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .impact(SimpleScore.ONE) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .impact(SimpleScore.ONE, (entity, entity2, value, value2) -> 2) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2514,38 +2483,36 @@ public void impactPositiveBigDecimalCustomJustifications() { (entity, entity2, value, value2) -> BigDecimal.valueOf(2)) .justifyWith( (a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactNegativeCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) .impact(SimpleScore.ONE, (entity, entity2, value, value2) -> -2) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactNegativeBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2557,13 +2524,12 @@ public void impactNegativeBigDecimalCustomJustifications() { (entity, entity2, value, value2) -> BigDecimal.valueOf(-2)) .justifyWith( (a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @@ -2576,24 +2542,8 @@ public void failWithMultipleJustifications() { .penalize(SimpleScore.ONE, (entity, entity2, value, value2) -> 2) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) .asConstraint(TEST_CONSTRAINT_NAME))) .hasMessageContaining("Maybe the constraint calls justifyWith() twice?"); } - @Override - @TestTemplate - public void failWithMultipleIndictments() { - assertThatCode(() -> buildScoreDirector( - factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) - .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) - .join(TestdataLavishValue.class, equal((entity, entity2, value) -> value, identity())) - .penalize(SimpleScore.ONE, (entity, entity2, value, value2) -> 2) - .justifyWith((a, b, c, d, score) -> new TestConstraintJustification<>(score, a, b, c, d)) - .indictWith(List::of) - .indictWith(List::of) - .asConstraint(TEST_CONSTRAINT_NAME))) - .hasMessageContaining("Maybe the constraint calls indictWith() twice?"); - } - } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderTest.java index f9f0751111f..9ff29e56217 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/QuadConstraintBuilderTest.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.quad; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.stream.AbstractConstraintBuilderTest; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; @@ -19,10 +19,9 @@ class QuadConstraintBuilderTest extends AbstractConstraintBuilderTest { protected AbstractConstraintBuilder of(String constraintName, String constraintGroup) { return new QuadConstraintBuilderImpl<>( (constraintName1, constraintDescription, constraintGroup1, constraintWeight, impactType, - objectSimpleScoreObjectBiFunction, - objectCollectionFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, + objectSimpleScoreObjectBiFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, ConstraintRef.of(constraintName1), constraintDescription, constraintGroup1, constraintWeight, - impactType, objectSimpleScoreObjectBiFunction, objectCollectionFunction, null), + impactType, objectSimpleScoreObjectBiFunction, null), ScoreImpactType.PENALTY, SimpleScore.ONE); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index 29db8af2d11..80441cdcfa3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -18,22 +18,18 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamFunctionalTest; @@ -62,23 +58,23 @@ protected AbstractTriConstraintStreamTest(ConstraintStreamImplSupport implSuppor @Override @TestTemplate public void filter_entity() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value1); + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity3); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> { + var scoreDirector = buildScoreDirector(factory -> { return factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal(TestdataLavishEntity::getValue, identity())) .join(TestdataLavishExtra.class) @@ -113,14 +109,14 @@ public void filter_entity() { @Override @TestTemplate public void filter_consecutive() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(5, 5); - TestdataLavishEntity entity1 = solution.getEntityList().get(0); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntity entity4 = solution.getEntityList().get(3); - TestdataLavishEntity entity5 = solution.getEntityList().get(4); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(5, 5); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); + var entity5 = solution.getEntityList().get(4); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((a, b) -> a, identity()), @@ -145,26 +141,26 @@ public void filter_consecutive() { @Override @TestTemplate public void join_0() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value2); + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity3); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); solution.getExtraList().add(extra3); // pick three distinct entities and join them with all extra values - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((a, b) -> Stream.of(entity1, entity2, entity3) .filter(entity -> !Objects.equals(entity, a)) @@ -199,32 +195,32 @@ public void join_0() { @Override @TestTemplate public void join_1Equal() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); entity1.setStringProperty("MyString"); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); entity2.setStringProperty(null); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value2); + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value2); entity3.setStringProperty(null); solution.getEntityList().add(entity3); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); extra1.setStringProperty("MyString"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); extra2.setStringProperty(null); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); extra3.setStringProperty("MyString"); solution.getExtraList().add(extra3); // pick three distinct entities and join them with an extra value that matches that of the first entity - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((a, b) -> Stream.of(entity1, entity2, entity3) .filter(entity -> !Objects.equals(entity, a)) @@ -256,33 +252,33 @@ public void join_1Equal() { @Override @TestTemplate public void join_2Equal() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); entity1.setStringProperty("MyString"); entity1.setIntegerProperty(7); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); entity2.setStringProperty(null); entity2.setIntegerProperty(8); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); extra1.setStringProperty("MyString"); extra1.setIntegerProperty(8); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); extra2.setStringProperty(null); extra2.setIntegerProperty(7); solution.getExtraList().add(extra2); - TestdataLavishExtra extra3 = new TestdataLavishExtra("MyExtra 3"); + var extra3 = new TestdataLavishExtra("MyExtra 3"); extra3.setStringProperty("MyString"); extra3.setIntegerProperty(7); solution.getExtraList().add(extra3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishValue.class, equal((e1, e2) -> e1.getValue(), identity())) .join(TestdataLavishExtra.class, @@ -306,21 +302,21 @@ public void join_2Equal() { @Override @TestTemplate public void joinAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .groupBy(countDistinct(TestdataLavishEntity::getValue), countDistinct(TestdataLavishEntity::getValue), @@ -368,11 +364,11 @@ public void ifExists_unknownClass() { @Override @TestTemplate public void ifExists_0Joiner0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishValueGroup valueGroup = new TestdataLavishValueGroup("MyValueGroup"); + var solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); + var valueGroup = new TestdataLavishValueGroup("MyValueGroup"); solution.getValueGroupList().add(valueGroup); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishValueGroup.class) .join(TestdataLavishEntityGroup.class) .ifExists(TestdataLavishEntity.class) @@ -385,7 +381,7 @@ public void ifExists_0Joiner0Filter() { assertMatch(valueGroup, solution.getFirstValueGroup(), solution.getFirstEntityGroup())); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityGroupList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -395,16 +391,16 @@ public void ifExists_0Joiner0Filter() { @Override @TestTemplate public void ifExists_0Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> !Objects.equals(entityC, entityA) && @@ -421,7 +417,7 @@ public void ifExists_0Join1Filter() { assertMatch(entity1, entity2, solution.getFirstEntity())); // Incremental - TestdataLavishEntityGroup toRemove = solution.getFirstEntityGroup(); + var toRemove = solution.getFirstEntityGroup(); scoreDirector.beforeProblemFactRemoved(toRemove); solution.getEntityGroupList().remove(toRemove); scoreDirector.afterProblemFactRemoved(toRemove); @@ -431,16 +427,16 @@ public void ifExists_0Join1Filter() { @Override @TestTemplate public void ifExists_1Join0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> !Objects.equals(entityC, entityA) && @@ -469,16 +465,16 @@ public void ifExists_1Join0Filter() { @Override @TestTemplate public void ifExists_1Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> !Objects.equals(entityC, entityA) && @@ -505,19 +501,19 @@ public void ifExists_1Join1Filter() { @Override @TestTemplate public void ifExistsDoesNotIncludeUnassigned() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("Entity with null var", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("Entity with null var", solution.getFirstEntityGroup(), null); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> entityA != entityC && entityB != entityC)) @@ -548,11 +544,11 @@ public void ifNotExists_unknownClass() { @Override @TestTemplate public void ifNotExists_0Joiner0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishValueGroup valueGroup = new TestdataLavishValueGroup("MyValueGroup"); + var solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); + var valueGroup = new TestdataLavishValueGroup("MyValueGroup"); solution.getValueGroupList().add(valueGroup); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishValueGroup.class) .join(TestdataLavishEntityGroup.class) .ifNotExists(TestdataLavishEntity.class) @@ -564,7 +560,7 @@ public void ifNotExists_0Joiner0Filter() { assertScore(scoreDirector); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityGroupList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -575,16 +571,16 @@ public void ifNotExists_0Joiner0Filter() { @Override @TestTemplate public void ifNotExists_0Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> !Objects.equals(entityC, entityA) && @@ -602,7 +598,7 @@ public void ifNotExists_0Join1Filter() { assertMatch(solution.getFirstEntity(), entity1, entity2)); // Incremental - TestdataLavishEntityGroup toRemove = solution.getFirstEntityGroup(); + var toRemove = solution.getFirstEntityGroup(); scoreDirector.beforeProblemFactRemoved(toRemove); solution.getEntityGroupList().remove(toRemove); scoreDirector.afterProblemFactRemoved(toRemove); @@ -615,16 +611,16 @@ public void ifNotExists_0Join1Filter() { @Override @TestTemplate public void ifNotExists_1Join0Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> !Objects.equals(entityC, entityA) && @@ -649,16 +645,16 @@ public void ifNotExists_1Join0Filter() { @Override @TestTemplate public void ifNotExists_1Join1Filter() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> !Objects.equals(entityC, entityA) && @@ -689,19 +685,19 @@ public void ifNotExists_1Join1Filter() { @Override @TestTemplate public void ifNotExistsDoesNotIncludeUnassigned() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishEntityGroup entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var entityGroup = new TestdataLavishEntityGroup("MyEntityGroup"); solution.getEntityGroupList().add(entityGroup); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); + var entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup, solution.getFirstValue()); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), solution.getFirstValue()); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("Entity with null var", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("Entity with null var", solution.getFirstEntityGroup(), null); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((entityA, entityB, entityC) -> entityA != entityC && entityB != entityC)) @@ -731,21 +727,21 @@ public void ifNotExistsDoesNotIncludeUnassigned() { @Override @TestTemplate public void ifExistsAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); - TestdataLavishValue value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(1, 0, 1, 0); + var value1 = new TestdataLavishValue("MyValue 1", solution.getFirstValueGroup()); solution.getValueList().add(value1); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); solution.getValueList().add(value2); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); + var entity1 = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value1); solution.getEntityList().add(entity2); - TestdataLavishExtra extra1 = new TestdataLavishExtra("MyExtra 1"); + var extra1 = new TestdataLavishExtra("MyExtra 1"); solution.getExtraList().add(extra1); - TestdataLavishExtra extra2 = new TestdataLavishExtra("MyExtra 2"); + var extra2 = new TestdataLavishExtra("MyExtra 2"); solution.getExtraList().add(extra2); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .groupBy(countDistinct(TestdataLavishEntity::getValue), countDistinct(TestdataLavishEntity::getValue), @@ -777,9 +773,9 @@ public void ifExistsAfterGroupBy() { @Override @TestTemplate public void groupBy_0Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -793,7 +789,7 @@ public void groupBy_0Mapping1Collector() { assertMatchWithScore(-3, 3L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -804,8 +800,8 @@ public void groupBy_0Mapping1Collector() { @Override @TestTemplate public void groupBy_0Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((e1, e2) -> e1, Function.identity())) .groupBy(countTri(), @@ -813,7 +809,7 @@ public void groupBy_0Mapping2Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); // From scratch scoreDirector.setWorkingSolution(solution); @@ -829,8 +825,8 @@ public void groupBy_0Mapping2Collector() { @Override @TestTemplate public void groupBy_0Mapping3Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((e1, e2) -> e1, Function.identity())) .groupBy(countTri(), @@ -841,11 +837,11 @@ public void groupBy_0Mapping3Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(0L); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(1L); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity3 = solution.getEntityList().get(2); entity3.setLongProperty(2L); // From scratch @@ -864,8 +860,8 @@ public void groupBy_0Mapping3Collector() { @Override @TestTemplate public void groupBy_0Mapping4Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((e1, e2) -> e1, Function.identity())) .groupBy(countTri(), @@ -877,11 +873,11 @@ public void groupBy_0Mapping4Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(0L); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(1L); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity3 = solution.getEntityList().get(2); entity3.setLongProperty(2L); // From scratch @@ -900,9 +896,9 @@ public void groupBy_0Mapping4Collector() { @Override @TestTemplate public void groupBy_1Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -910,8 +906,8 @@ public void groupBy_1Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, @@ -922,9 +918,9 @@ public void groupBy_1Mapping0Collector() { @Override @TestTemplate public void groupBy_1Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -932,8 +928,8 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE, (group, count) -> count) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -942,7 +938,7 @@ public void groupBy_1Mapping1Collector() { assertMatchWithScore(-2, value1, 2L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -954,9 +950,9 @@ public void groupBy_1Mapping1Collector() { @Override @TestTemplate public void groupBy_1Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((entityA, entityB) -> entityA, Function.identity())) .groupBy((entityA, entityB, entityC) -> entityA.toString(), @@ -965,30 +961,30 @@ public void groupBy_1Mapping2Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); } @Override @TestTemplate public void groupBy_1Mapping3Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, equal((entityA, entityB) -> entityA, Function.identity())) .groupBy((entityA, entityB, entityC) -> entityA.toString(), @@ -1000,34 +996,34 @@ public void groupBy_1Mapping3Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); entity1.setLongProperty(Long.MAX_VALUE); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); + var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, + assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); } @Override @TestTemplate public void groupBy_2Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -1035,10 +1031,10 @@ public void groupBy_2Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1047,7 +1043,7 @@ public void groupBy_2Mapping0Collector() { assertMatchWithScore(-1, group1, value1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1059,9 +1055,9 @@ public void groupBy_2Mapping0Collector() { @Override @TestTemplate public void groupBy_2Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -1069,10 +1065,10 @@ public void groupBy_2Mapping1Collector() { .penalize(SimpleScore.ONE, (group, value, count) -> count) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1081,7 +1077,7 @@ public void groupBy_2Mapping1Collector() { assertMatchWithScore(-2, group1, value1, 2L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1093,9 +1089,9 @@ public void groupBy_2Mapping1Collector() { @Override @TestTemplate public void groupBy_2Mapping2Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); + var solution = TestdataLavishSolution.generateSolution(1, 2, 2, 3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntityGroup.class, equal(TestdataLavishEntity::getEntityGroup, identity())) .join(TestdataLavishValue.class, equal((entity, group) -> entity.getValue(), identity())) @@ -1104,10 +1100,10 @@ public void groupBy_2Mapping2Collector() { (group, value, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1116,7 +1112,7 @@ public void groupBy_2Mapping2Collector() { assertMatchWithScore(-4, group1, value1, 2L, 2L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1128,8 +1124,8 @@ public void groupBy_2Mapping2Collector() { @Override @TestTemplate public void groupBy_3Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) @@ -1137,9 +1133,9 @@ public void groupBy_3Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1149,7 +1145,7 @@ public void groupBy_3Mapping0Collector() { assertMatchWithScore(-1, group2, group3, group1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1159,8 +1155,8 @@ public void groupBy_3Mapping0Collector() { @Override @TestTemplate public void groupBy_3Mapping1Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) @@ -1169,9 +1165,9 @@ public void groupBy_3Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1181,7 +1177,7 @@ public void groupBy_3Mapping1Collector() { assertMatchWithScore(-1, group2, group3, group1, 1L)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1191,8 +1187,8 @@ public void groupBy_3Mapping1Collector() { @Override @TestTemplate public void groupBy_4Mapping0Collector() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); - InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory + var solution = TestdataLavishSolution.generateSolution(2, 2, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory .forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) @@ -1201,11 +1197,11 @@ public void groupBy_4Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getEntityGroupList().get(0); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); - TestdataLavishValue value1 = solution.getValueList().get(0); - TestdataLavishValue value2 = solution.getValueList().get(1); + var group1 = solution.getEntityGroupList().get(0); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); + var value1 = solution.getValueList().get(0); + var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1215,7 +1211,7 @@ public void groupBy_4Mapping0Collector() { assertMatchWithScore(-1, group2, group3, group1, value1)); // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1225,17 +1221,17 @@ public void groupBy_4Mapping0Collector() { @Override @TestTemplate public void distinct() { // On a distinct stream, this is a no-op. - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(2, 2, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .distinct() .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1248,16 +1244,16 @@ public void distinct() { // On a distinct stream, this is a no-op. @Override @TestTemplate public void mapToUniWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> asSet(a.getEntityGroup(), b.getEntityGroup(), c.getEntityGroup())) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1266,7 +1262,7 @@ public void mapToUniWithDuplicates() { assertMatch(asSet(group1, group2)), assertMatch(asSet(group1, group2))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1278,17 +1274,17 @@ public void mapToUniWithDuplicates() { @Override @TestTemplate public void mapToUniWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> asSet(a.getEntityGroup(), b.getEntityGroup())) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1297,7 +1293,7 @@ public void mapToUniWithoutDuplicates() { assertMatch(asSet(group1, group3)), assertMatch(asSet(group2, group3))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1309,8 +1305,8 @@ public void mapToUniWithoutDuplicates() { @Override @TestTemplate public void mapToUniAndDistinctWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> asSet(a.getEntityGroup(), b.getEntityGroup(), c.getEntityGroup())) @@ -1318,15 +1314,15 @@ public void mapToUniAndDistinctWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(asSet(group1, group2))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1338,8 +1334,8 @@ public void mapToUniAndDistinctWithDuplicates() { @Override @TestTemplate public void mapToUniAndDistinctWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> asSet(a.getEntityGroup(), b.getEntityGroup())) @@ -1347,9 +1343,9 @@ public void mapToUniAndDistinctWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1358,7 +1354,7 @@ public void mapToUniAndDistinctWithoutDuplicates() { assertMatch(asSet(group1, group3)), assertMatch(asSet(group2, group3))); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1370,8 +1366,8 @@ public void mapToUniAndDistinctWithoutDuplicates() { @Override @TestTemplate public void mapToBi() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> a.getEntityGroup(), @@ -1379,9 +1375,9 @@ public void mapToBi() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1390,7 +1386,7 @@ public void mapToBi() { assertMatch(group1, group3), assertMatch(group2, group3)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1402,8 +1398,8 @@ public void mapToBi() { @Override @TestTemplate public void mapToTri() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> a.getEntityGroup(), @@ -1412,9 +1408,9 @@ public void mapToTri() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); @@ -1423,7 +1419,7 @@ public void mapToTri() { assertMatch(group1, group3, group2), assertMatch(group2, group3, group1)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1435,8 +1431,8 @@ public void mapToTri() { @Override @TestTemplate public void mapToQuad() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .map((a, b, c) -> a.getEntityGroup(), @@ -1446,10 +1442,10 @@ public void mapToQuad() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - TestdataLavishEntityGroup group3 = solution.getEntityGroupList().get(2); - long sum = solution.getEntityList().stream() + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + var group3 = solution.getEntityGroupList().get(2); + var sum = solution.getEntityList().stream() .mapToLong(TestdataLavishEntity::getLongProperty) .sum(); @@ -1460,7 +1456,7 @@ public void mapToQuad() { assertMatch(group1, group3, group2, sum), assertMatch(group2, group3, group1, sum)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1472,18 +1468,18 @@ public void mapToQuad() { @Override @TestTemplate public void expandToQuad() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 3, 3); + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .expand((a, b, c) -> a.getLongProperty() + b.getLongProperty() + c.getLongProperty()) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - long sum = solution.getEntityList().stream() + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var sum = solution.getEntityList().stream() .mapToLong(TestdataLavishEntity::getLongProperty) .sum(); @@ -1494,7 +1490,7 @@ public void expandToQuad() { assertMatch(entity1, entity3, entity2, sum), assertMatch(entity2, entity3, entity1, sum)); - TestdataLavishEntity entity = solution.getFirstEntity(); + var entity = solution.getFirstEntity(); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1506,14 +1502,14 @@ public void expandToQuad() { @Override @TestTemplate public void flatten() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .flatten((a, b, c) -> asList(a.getEntityGroup(), b.getEntityGroup(), c.getEntityGroup())) @@ -1543,14 +1539,14 @@ public void flatten() { @Override @TestTemplate public void flattenLastWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .flattenLast(c -> asList(c.getEntityGroup(), group1, group2)) @@ -1580,14 +1576,14 @@ public void flattenLastWithDuplicates() { @Override @TestTemplate public void flattenLastWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .flattenLast(c -> asSet(c.getEntityGroup(), c.getEntityGroup() == group1 ? group2 : group1)) @@ -1614,14 +1610,14 @@ public void flattenLastWithoutDuplicates() { @Override @TestTemplate public void flattenLastAndDistinctWithDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .flattenLast(c -> asList(c.getEntityGroup(), group1, group2)) @@ -1649,14 +1645,14 @@ public void flattenLastAndDistinctWithDuplicates() { @Override @TestTemplate public void flattenLastAndDistinctWithoutDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - TestdataLavishEntity entity3 = solution.getEntityList().get(2); - TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); - TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); - - InnerScoreDirector scoreDirector = + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) .flattenLast(c -> asSet(c.getEntityGroup(), c.getEntityGroup() == group1 ? group2 : group1)) @@ -1684,21 +1680,21 @@ public void flattenLastAndDistinctWithoutDuplicates() { @Override @TestTemplate public void concatUniWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1732,21 +1728,21 @@ public void concatUniWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctUniWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1781,21 +1777,21 @@ public void concatAndDistinctUniWithoutValueDuplicates() { @Override @TestTemplate public void concatBiWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1831,21 +1827,21 @@ public void concatBiWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctBiWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1882,21 +1878,21 @@ public void concatAndDistinctBiWithoutValueDuplicates() { @Override @TestTemplate public void concatTriWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1934,21 +1930,21 @@ public void concatTriWithoutValueDuplicates() { @Override @TestTemplate public void concatTriWithValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -1986,21 +1982,21 @@ public void concatTriWithValueDuplicates() { @Override @TestTemplate public void concatAndDistinctTriWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2039,21 +2035,21 @@ public void concatAndDistinctTriWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctTriWithValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2090,21 +2086,21 @@ public void concatAndDistinctTriWithValueDuplicates() { @Override @TestTemplate public void concatQuadWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2144,21 +2140,21 @@ public void concatQuadWithoutValueDuplicates() { @Override @TestTemplate public void concatAndDistinctQuadWithoutValueDuplicates() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .filter(entity -> entity.getValue() == value1) .join(factory.forEach(TestdataLavishEntity.class) @@ -2199,21 +2195,21 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { @Override @TestTemplate public void concatAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); - TestdataLavishValue value1 = solution.getFirstValue(); - TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); - TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); + var value1 = solution.getFirstValue(); + var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); solution.getValueList().add(value2); solution.getValueList().add(value3); - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), + var entity1 = solution.getFirstEntity(); + var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), + var entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), value3); solution.getEntityList().add(entity3); - InnerScoreDirector scoreDirector = + var scoreDirector = buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) .join(TestdataLavishEntity.class) .join(TestdataLavishEntity.class) @@ -2306,9 +2302,9 @@ public void complement() { @Override @TestTemplate public void penalizeUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .penalize(SimpleScore.ONE) @@ -2316,13 +2312,13 @@ public void penalizeUnweighted() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-2)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void penalizeUnweightedBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2335,41 +2331,31 @@ public void penalizeUnweightedBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-2))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } - private , Solution_, Entity_, Value_> void assertDefaultJustifications( - InnerScoreDirector scoreDirector, List entityList, List valueList) { + private , Solution_> void + assertDefaultJustifications(InnerScoreDirector scoreDirector) { if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList.get(0), - entityList.get(1), - entityList.get(5), - entityList.get(6), - valueList.get(0), - valueList.get(1)); - - Map> constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + .containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(2); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); - for (int i = 0; i < 2; i++) { - ConstraintMatch constraintMatch = constraintMatchList.get(i); + for (var i = 0; i < 2; i++) { + var constraintMatch = constraintMatchList.get(i); assertSoftly(softly -> { - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(DefaultConstraintJustification.class); - DefaultConstraintJustification castJustification = + var castJustification = (DefaultConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .hasSize(3); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .hasSize(3); }); } } @@ -2377,9 +2363,9 @@ private , Solution_, Entity_, Value_> void assertDe @Override @TestTemplate public void penalize() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .penalize(SimpleScore.ONE, (entity, entity2, value) -> 2) @@ -2387,13 +2373,13 @@ public void penalize() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void penalizeBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2407,15 +2393,15 @@ public void penalizeBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void rewardUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .reward(SimpleScore.ONE) @@ -2423,15 +2409,15 @@ public void rewardUnweighted() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void reward() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .reward(SimpleScore.ONE, (entity, entity2, value) -> 2) @@ -2439,13 +2425,13 @@ public void reward() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void rewardBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2459,15 +2445,15 @@ public void rewardBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveUnweighted() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .impact(SimpleScore.ONE) @@ -2475,15 +2461,15 @@ public void impactPositiveUnweighted() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactPositive() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .impact(SimpleScore.ONE, (entity, entity2, value) -> 2) @@ -2491,13 +2477,13 @@ public void impactPositive() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2511,15 +2497,15 @@ public void impactPositiveBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactNegative() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .impact(SimpleScore.ONE, (entity, entity2, value) -> -2) @@ -2527,13 +2513,13 @@ public void impactNegative() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void impactNegativeBigDecimal() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2547,59 +2533,46 @@ public void impactNegativeBigDecimal() { scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertDefaultJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertDefaultJustifications(scoreDirector); } @Override @TestTemplate public void penalizeUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .penalize(SimpleScore.ONE) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-2)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } - private , Solution_, Entity_, Value_> void assertCustomJustifications( - InnerScoreDirector scoreDirector, List entityList, List valueList) { + private , Solution_> void + assertCustomJustifications(InnerScoreDirector scoreDirector) { if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList.get(0), - entityList.get(1), - entityList.get(5), - entityList.get(6), - valueList.get(0), - valueList.get(1)); - - Map> constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); - assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + assertThat(constraintMatchTotalMap).containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(2); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); - for (int i = 0; i < 2; i++) { - ConstraintMatch constraintMatch = constraintMatchList.get(i); + for (var i = 0; i < 2; i++) { + var constraintMatch = constraintMatchList.get(i); assertSoftly(softly -> { - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(TestConstraintJustification.class); - TestConstraintJustification castJustification = - (TestConstraintJustification) justification; + var castJustification = (TestConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .hasSize(3); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .hasSize(3); }); } } @@ -2607,25 +2580,24 @@ private , Solution_, Entity_, Value_> void assertCu @Override @TestTemplate public void penalizeCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .penalize(SimpleScore.ONE, (entity, entity2, value) -> 2) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void penalizeBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2635,55 +2607,52 @@ public void penalizeBigDecimalCustomJustifications() { .penalizeBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2, value) -> BigDecimal.valueOf(2)) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void rewardUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .reward(SimpleScore.ONE) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void rewardCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .reward(SimpleScore.ONE, (entity, entity2, value) -> 2) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void rewardBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2693,55 +2662,52 @@ public void rewardBigDecimalCustomJustifications() { .rewardBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2, value) -> BigDecimal.valueOf(2)) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveUnweightedCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .impact(SimpleScore.ONE) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(2)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .impact(SimpleScore.ONE, (entity, entity2, value) -> 2) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactPositiveBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2751,37 +2717,35 @@ public void impactPositiveBigDecimalCustomJustifications() { .impactBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2, value) -> BigDecimal.valueOf(2)) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactNegativeCustomJustifications() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(); + var solution = TestdataLavishSolution.generateSolution(); - InnerScoreDirector scoreDirector = buildScoreDirector( + var scoreDirector = buildScoreDirector( factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) .impact(SimpleScore.ONE, (entity, entity2, value) -> -2) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleScore.of(-4)); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @TestTemplate public void impactNegativeBigDecimalCustomJustifications() { - TestdataSimpleBigDecimalScoreSolution solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); + var solution = TestdataSimpleBigDecimalScoreSolution.generateSolution(); InnerScoreDirector scoreDirector = buildScoreDirector(TestdataSimpleBigDecimalScoreSolution.buildSolutionDescriptor(), @@ -2791,13 +2755,12 @@ public void impactNegativeBigDecimalCustomJustifications() { .impactBigDecimal(SimpleBigDecimalScore.ONE, (entity, entity2, value) -> BigDecimal.valueOf(-2)) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); assertThat(scoreDirector.calculateScore().raw()).isEqualTo(SimpleBigDecimalScore.of(BigDecimal.valueOf(-4))); - assertCustomJustifications(scoreDirector, solution.getEntityList(), solution.getValueList()); + assertCustomJustifications(scoreDirector); } @Override @@ -2809,22 +2772,8 @@ public void failWithMultipleJustifications() { .penalize(SimpleScore.ONE, (entity, entity2, value) -> 2) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME))) .hasMessageContaining("Maybe the constraint calls justifyWith() twice?"); } - @Override - @TestTemplate - public void failWithMultipleIndictments() { - assertThatCode(() -> buildScoreDirector( - factory -> factory.forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getValue)) - .join(TestdataLavishValue.class, equal((entity, entity2) -> entity.getValue(), identity())) - .penalize(SimpleScore.ONE, (entity, entity2, value) -> 2) - .justifyWith((a, b, c, score) -> new TestConstraintJustification<>(score, a, b, c)) - .indictWith(Set::of) - .indictWith(Set::of) - .asConstraint(TEST_CONSTRAINT_NAME))) - .hasMessageContaining("Maybe the constraint calls indictWith() twice?"); - } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderTest.java index 9b6dad39061..fbf4f847714 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/TriConstraintBuilderTest.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.tri; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.stream.AbstractConstraintBuilderTest; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; @@ -19,10 +19,9 @@ class TriConstraintBuilderTest extends AbstractConstraintBuilderTest { protected AbstractConstraintBuilder of(String constraintName, String constraintGroup) { return new TriConstraintBuilderImpl<>( (constraintName1, constraintDescription, constraintGroup1, constraintWeight, impactType, - objectSimpleScoreObjectBiFunction, - objectCollectionFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, + objectSimpleScoreObjectBiFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, ConstraintRef.of(constraintName1), constraintDescription, constraintGroup1, constraintWeight, - impactType, objectSimpleScoreObjectBiFunction, objectCollectionFunction, null), + impactType, objectSimpleScoreObjectBiFunction, null), ScoreImpactType.PENALTY, SimpleScore.ONE); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 8b6ef0a6a60..d117d0f96e9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -21,7 +21,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Stream; @@ -30,12 +29,12 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamFunctionalTest; @@ -3108,13 +3107,9 @@ private , Solution_, Entity_> void assertDefaultJus if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList); - var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); - assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + assertThat(constraintMatchTotalMap).containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(entityList.size()); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); @@ -3125,12 +3120,9 @@ private , Solution_, Entity_> void assertDefaultJus var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(DefaultConstraintJustification.class); - var castJustification = - (DefaultConstraintJustification) justification; + var castJustification = (DefaultConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .containsExactly(entity); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .containsExactly(entity); }); } } @@ -3306,7 +3298,6 @@ public void penalizeUnweightedCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .penalize(SimpleScore.ONE) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3319,13 +3310,10 @@ private , Solution_, Entity_> void assertCustomJust if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; - assertThat(scoreDirector.getIndictmentMap()) - .containsOnlyKeys(entityList); - var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); assertThat(constraintMatchTotalMap) - .containsOnlyKeys(TEST_CONSTRAINT_NAME); - var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_NAME); + .containsOnlyKeys(TEST_CONSTRAINT_REF); + var constraintMatchTotal = constraintMatchTotalMap.get(TEST_CONSTRAINT_REF); assertThat(constraintMatchTotal.getConstraintMatchSet()) .hasSize(entityList.size()); List> constraintMatchList = new ArrayList<>(constraintMatchTotal.getConstraintMatchSet()); @@ -3336,12 +3324,9 @@ private , Solution_, Entity_> void assertCustomJust var justification = constraintMatch.getJustification(); softly.assertThat(justification) .isInstanceOf(TestConstraintJustification.class); - var castJustification = - (TestConstraintJustification) justification; + var castJustification = (TestConstraintJustification) justification; softly.assertThat(castJustification.getFacts()) .containsExactly(entity); - softly.assertThat(constraintMatch.getIndictedObjectList()) - .containsExactly(entity); }); } } @@ -3355,7 +3340,6 @@ public void penalizeCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .penalize(SimpleScore.ONE, entity -> 2) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3373,7 +3357,6 @@ public void penalizeBigDecimalCustomJustifications() { factory -> new Constraint[] { factory.forEach(TestdataEntity.class) .penalizeBigDecimal(SimpleBigDecimalScore.ONE, entity -> BigDecimal.valueOf(2)) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); scoreDirector.setWorkingSolution(solution); @@ -3390,7 +3373,6 @@ public void rewardUnweightedCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .reward(SimpleScore.ONE) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3407,7 +3389,6 @@ public void rewardCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .reward(SimpleScore.ONE, entity -> 2) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3426,7 +3407,6 @@ public void rewardBigDecimalCustomJustifications() { factory.forEach(TestdataEntity.class) .rewardBigDecimal(SimpleBigDecimalScore.ONE, entity -> BigDecimal.valueOf(2)) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -3444,7 +3424,6 @@ public void impactPositiveUnweightedCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .impact(SimpleScore.ONE) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3461,7 +3440,6 @@ public void impactPositiveCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .impact(SimpleScore.ONE, entity -> 2) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3481,7 +3459,6 @@ public void impactPositiveBigDecimalCustomJustifications() { .impactBigDecimal(SimpleBigDecimalScore.ONE, entity -> BigDecimal.valueOf(2)) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -3499,7 +3476,6 @@ public void impactNegativeCustomJustifications() { factory -> factory.forEach(TestdataLavishEntity.class) .impact(SimpleScore.ONE, entity -> -2) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME)); scoreDirector.setWorkingSolution(solution); @@ -3519,7 +3495,6 @@ public void impactNegativeBigDecimalCustomJustifications() { .impactBigDecimal(SimpleBigDecimalScore.ONE, entity -> BigDecimal.valueOf(-2)) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME) }); @@ -3536,24 +3511,10 @@ public void failWithMultipleJustifications() { .penalize(SimpleScore.ONE, entity -> 2) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) .asConstraint(TEST_CONSTRAINT_NAME))) .hasMessageContaining("Maybe the constraint calls justifyWith() twice?"); } - @Override - @TestTemplate - public void failWithMultipleIndictments() { - assertThatCode(() -> buildScoreDirector( - factory -> factory.forEach(TestdataLavishEntity.class) - .penalize(SimpleScore.ONE, entity -> 2) - .justifyWith((a, score) -> new TestConstraintJustification<>(score, a)) - .indictWith(Set::of) - .indictWith(Set::of) - .asConstraint(TEST_CONSTRAINT_NAME))) - .hasMessageContaining("Maybe the constraint calls indictWith() twice?"); - } - // ************************************************************************ // Combinations // ************************************************************************ @@ -3631,7 +3592,7 @@ public void constraintProvidedFromUnknownPackage() throws ClassNotFoundException scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch("Always penalize", entityList.get(0))); + assertMatch("Always penalize", entityList.getFirst())); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderTest.java index 4940ec452c1..b6f66fbda88 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/UniConstraintBuilderTest.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.uni; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.stream.AbstractConstraintBuilderTest; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; @@ -19,10 +19,9 @@ class UniConstraintBuilderTest extends AbstractConstraintBuilderTest { protected AbstractConstraintBuilder of(String constraintName, String constraintGroup) { return new UniConstraintBuilderImpl<>( (constraintName1, constraintDescription, constraintGroup1, constraintWeight, impactType, - objectSimpleScoreObjectBiFunction, - objectCollectionFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, + objectSimpleScoreObjectBiFunction) -> new BavetConstraint<>(CONSTRAINT_FACTORY, ConstraintRef.of(constraintName1), constraintDescription, constraintGroup1, constraintWeight, - impactType, objectSimpleScoreObjectBiFunction, objectCollectionFunction, null), + impactType, objectSimpleScoreObjectBiFunction, null), ScoreImpactType.PENALTY, SimpleScore.ONE); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/test/SingleConstraintAssertionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/test/SingleConstraintAssertionTest.java index 084ede596b6..cd6477fcd04 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/test/SingleConstraintAssertionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/test/SingleConstraintAssertionTest.java @@ -117,14 +117,6 @@ void triggerVariableListenersListSingleSolution() { .settingAllShadowVariables() .justifiesWith(DefaultConstraintJustification.of(SimpleScore.of(-10), solution.getValueList().getFirst()))) .doesNotThrowAnyException(); - - // Test cascade indictment - assertThatCode(() -> shadowConstraintVerifier - .verifyThat(TestdataListMultipleShadowVariableConstraintProvider::penalizeCascadingUpdate) - .givenSolution(solution) - .settingAllShadowVariables() - .indictsWith(solution.getValueList().getFirst())) - .doesNotThrowAnyException(); } @Test @@ -1110,312 +1102,6 @@ void justifiesExactlyEmptyMatches() { .hasMessageContaining("Expected but not found:"); } - @Test - void indicts() { - var solution = TestdataConstraintVerifierSolution.generateSolution(2, 3); - - // No error - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWith(solution.getEntityList().toArray())) - .doesNotThrowAnyException(); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWith(solution.getEntityList().toArray())) - .doesNotThrowAnyException(); - - // Invalid indictment - var badEntity = - new TestdataConstraintVerifierFirstEntity("bad code", new TestdataValue("bad code")); - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWith(badEntity)) - .hasMessageContaining("Broken expectation") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:"); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWith(badEntity)) - .hasMessageContaining("Broken expectation") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:"); - - // Multiple indictments - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWith(solution.getEntityList().getFirst(), badEntity)) - .hasMessageContaining("Broken expectation") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:"); - - // Invalid matches and classes - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWith(solution.getEntityList().getFirst(), badEntity, "bad indictment")) - .hasMessageContaining("Broken expectation") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 0')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='bad code')") - .hasMessageContaining("bad indictment") - .hasMessageContaining("Actual") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 0')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 1')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 2')") - .hasMessageContaining("Expected but not found:") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='bad code')") - .hasMessageContaining("bad indictment"); - } - - @Test - void indictsWithCustomMessage() { - var solution = TestdataConstraintVerifierSolution.generateSolution(2, 3); - - var badEntity = - new TestdataConstraintVerifierFirstEntity("bad code", new TestdataValue("bad code")); - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWith("Custom Message", badEntity)) - .hasMessageContaining("Custom Message") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:"); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWith("Custom Message", badEntity)) - .hasMessageContaining("Custom Message") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:"); - } - - @Test - void indictEmptyMatches() { - var solution = TestdataConstraintVerifierSolution.generateSolution(2, 3); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithNoJustifications) - .given(solution.getEntityList().toArray()) - .indictsWith()) - .doesNotThrowAnyException(); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWith()) - .hasMessageContaining("Broken expectation") - .hasMessageContaining("Expected") - .hasMessageContaining("No Indictment") - .hasMessageContaining("Actual") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 0')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 1')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 2')") - .hasMessageContaining("Unexpected but found:"); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithNoJustifications) - .given(solution.getEntityList().toArray()) - .indictsWith(new TestFirstJustification("1"))) - .hasMessageContaining("Broken expectation") - .hasMessageContaining("Expected") - .hasMessageContaining("TestFirstJustification[id=1]") - .hasMessageContaining("Actual") - .hasMessageContaining("No Indictment") - .hasMessageContaining("Expected but not found:"); - } - - @Test - void indictsWithExactly() { - var solution = TestdataConstraintVerifierSolution.generateSolution(2, 3); - - // No error - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWithExactly(solution.getEntityList().toArray())) - .doesNotThrowAnyException(); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWithExactly(solution.getEntityList().toArray())) - .doesNotThrowAnyException(); - - // Invalid indictment - var badEntity = - new TestdataConstraintVerifierFirstEntity("bad code", new TestdataValue("bad code")); - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWithExactly(badEntity)) - .hasMessageContaining("Broken expectation") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:"); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWithExactly(badEntity)) - .hasMessageContaining("Broken expectation") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:") - .hasMessageContaining("Unexpected but found:"); - } - - @Test - void indictsWithExactlyWithCustomMessage() { - var solution = TestdataConstraintVerifierSolution.generateSolution(2, 3); - - var badEntity = - new TestdataConstraintVerifierFirstEntity("bad code", new TestdataValue("bad code")); - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWithExactly("Custom Message", badEntity)) - .hasMessageContaining("Custom Message") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:") - .hasMessageContaining("Unexpected but found:"); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .givenSolution(solution) - .indictsWithExactly("Custom Message", badEntity)) - .hasMessageContaining("Custom Message") - .hasMessageContaining( - "Indictment: Justify with first justification") - .hasMessageContaining("Expected") - .hasMessageContaining(badEntity.toString()) - .hasMessageContaining("Actual") - .hasMessageContaining(solution.getEntityList().get(0).toString()) - .hasMessageContaining(solution.getEntityList().get(1).toString()) - .hasMessageContaining(solution.getEntityList().get(2).toString()) - .hasMessageContaining("Expected but not found:") - .hasMessageContaining("Unexpected but found:"); - } - - @Test - void indictsWithExactlyEmptyMatches() { - var solution = TestdataConstraintVerifierSolution.generateSolution(2, 3); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithNoJustifications) - .given(solution.getEntityList().toArray()) - .indictsWithExactly()) - .doesNotThrowAnyException(); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithFirstJustification) - .given(solution.getEntityList().toArray()) - .indictsWithExactly()) - .hasMessageContaining("Broken expectation") - .hasMessageContaining("Expected") - .hasMessageContaining("No Indictment") - .hasMessageContaining("Actual") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 0')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 1')") - .hasMessageContaining("TestdataConstraintVerifierFirstEntity(code='Generated Entity 2')") - .hasMessageContaining("Unexpected but found:"); - - assertThatCode( - () -> constraintVerifierForJustification - .verifyThat(TestdataConstraintVerifierJustificationProvider::justifyWithNoJustifications) - .given(solution.getEntityList().toArray()) - .indictsWithExactly(new TestFirstJustification("1"))) - .hasMessageContaining("Broken expectation") - .hasMessageContaining("Expected") - .hasMessageContaining("TestFirstJustification[id=1]") - .hasMessageContaining("Actual") - .hasMessageContaining("No Indictment") - .hasMessageContaining("Expected but not found:"); - } - @Test void shouldUpdateInternalConsistencyStateIfInconsistentIsNull() { var dependency = new TestdataDependencyValue("dependency", Duration.ofHours(1L)); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 7ab55b30b80..1791b3a2c1f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -7,12 +7,10 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; @@ -23,11 +21,7 @@ import ai.timefold.solver.core.api.score.HardSoftScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.api.solver.phase.PhaseCommand; @@ -68,8 +62,6 @@ import ai.timefold.solver.core.impl.heuristic.move.AbstractSelectorBasedMove; import ai.timefold.solver.core.impl.heuristic.selector.move.factory.MoveIteratorFactory; import ai.timefold.solver.core.impl.score.DummySimpleScoreEasyScoreCalculator; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; import ai.timefold.solver.core.impl.score.director.ScoreDirector; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; import ai.timefold.solver.core.impl.util.Pair; @@ -152,7 +144,6 @@ import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -402,50 +393,6 @@ void solveCorruptedEasyInitialized() { .hasMessageContaining("Score corruption analysis could not be generated"); } - @Test - void solveCorruptedIncrementalUninitialized() { - var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) - .withEnvironmentMode(EnvironmentMode.FULL_ASSERT) - .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(CorruptedIncrementalScoreCalculator.class)); - - var solution = new TestdataSolution("s1"); - solution.setValueList(Arrays.asList(new TestdataValue("v1"), new TestdataValue("v2"))); - solution.setEntityList(Arrays.asList(new TestdataEntity("e1"), new TestdataEntity("e2"))); - - Assertions.assertThatThrownBy(() -> PlannerTestUtils.solve(solverConfig, solution, false)) - .hasMessageContaining("Score corruption") - .hasMessageContaining("workingScore") - .hasMessageContaining("uncorruptedScore") - .hasMessageContaining("Score corruption analysis:"); - } - - @Test - void solveCorruptedIncrementalInitialized() { - var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) - .withEnvironmentMode(EnvironmentMode.FULL_ASSERT) - .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() - .withIncrementalScoreCalculatorClass(CorruptedIncrementalScoreCalculator.class)); - var solverFactory = SolverFactory. create(solverConfig); - var solver = solverFactory.buildSolver(); - - var solution = new TestdataSolution("s1"); - var value1 = new TestdataValue("v1"); - var value2 = new TestdataValue("v2"); - solution.setValueList(List.of(value1, value2)); - var entity1 = new TestdataEntity("e1"); - entity1.setValue(value1); - var entity2 = new TestdataEntity("e2"); - entity2.setValue(value2); - solution.setEntityList(List.of(entity1, entity2)); - - Assertions.assertThatThrownBy(() -> solver.solve(solution)) - .hasMessageContaining("Score corruption") - .hasMessageContaining("workingScore") - .hasMessageContaining("uncorruptedScore") - .hasMessageContaining("Score corruption analysis:"); - } - @Test void solveEmptyEntityList() { var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) @@ -2085,67 +2032,6 @@ public static class CorruptedEasyScoreCalculator implements EasyScoreCalculator< } } - public static class CorruptedIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - @Override - public void resetWorkingSolution(@NonNull TestdataSolution workingSolution, boolean constraintMatchEnabled) { - // Ignore - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - return Collections.singletonList(new DefaultConstraintMatchTotal<>(ConstraintRef.of("b"), SimpleScore.of(1))); - } - - @Override - public @Nullable Map> getIndictmentMap() { - return Collections.singletonMap(new TestdataEntity("e1"), - new DefaultIndictment<>(new TestdataEntity("e1"), SimpleScore.ONE)); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataSolution workingSolution) { - // Ignore - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - var random = new Random(); - return SimpleScore.of(random.nextInt(1000)); - } - } - @NullMarked public static final class TestingNeighborhoodProvider implements NeighborhoodProvider { diff --git a/core/src/test/java/ai/timefold/solver/core/testconstraint/TestConstraint.java b/core/src/test/java/ai/timefold/solver/core/testconstraint/TestConstraint.java index 3a674b5f450..5ef6003ea38 100644 --- a/core/src/test/java/ai/timefold/solver/core/testconstraint/TestConstraint.java +++ b/core/src/test/java/ai/timefold/solver/core/testconstraint/TestConstraint.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.testconstraint; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import ai.timefold.solver.core.impl.score.stream.common.ScoreImpactType; @@ -17,7 +17,7 @@ public TestConstraint(TestConstraintFactory constraintFactory public TestConstraint(TestConstraintFactory constraintFactory, String constraintName, String constraintGroup, Score_ constraintWeight) { super(constraintFactory, ConstraintRef.of(constraintName), "", constraintGroup, constraintWeight, - ScoreImpactType.REWARD, null, null); + ScoreImpactType.REWARD, null); } } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/TestdataIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/TestdataIncrementalScoreCalculator.java deleted file mode 100644 index cc2e8525c6f..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/TestdataIncrementalScoreCalculator.java +++ /dev/null @@ -1,104 +0,0 @@ -package ai.timefold.solver.core.testdomain; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -public class TestdataIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - private int score = 0; - private DefaultConstraintMatchTotal constraintMatchTotal; - private Map> indictmentMap; - - @Override - public void resetWorkingSolution(@NonNull TestdataSolution workingSolution) { - score = 0; - constraintMatchTotal = new DefaultConstraintMatchTotal<>(ConstraintRef.of("testConstraint"), SimpleScore.ONE); - indictmentMap = new HashMap<>(); - for (TestdataEntity left : workingSolution.getEntityList()) { - TestdataValue value = left.getValue(); - if (value == null) { - continue; - } - for (TestdataEntity right : workingSolution.getEntityList()) { - if (Objects.equals(right.getValue(), value)) { - score -= 1; - ConstraintMatch constraintMatch = - constraintMatchTotal.addConstraintMatch(List.of(left, right), SimpleScore.ONE); - Stream.of(left, right) - .forEach(entity -> indictmentMap - .computeIfAbsent(entity, key -> new DefaultIndictment<>(key, SimpleScore.ZERO)) - .getConstraintMatchSet() - .add(constraintMatch)); - } - } - } - } - - @Override - public void resetWorkingSolution(@NonNull TestdataSolution workingSolution, boolean constraintMatchEnabled) { - resetWorkingSolution(workingSolution); - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - return SimpleScore.of(score); - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - return Collections.singleton(constraintMatchTotal); - } - - @Override - public @Nullable Map> getIndictmentMap() { - return indictmentMap; - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/constraintverifier/TestdataConstraintVerifierJustificationProvider.java b/core/src/test/java/ai/timefold/solver/core/testdomain/constraintverifier/TestdataConstraintVerifierJustificationProvider.java index d86ebb96225..35ee836a5af 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/constraintverifier/TestdataConstraintVerifierJustificationProvider.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/constraintverifier/TestdataConstraintVerifierJustificationProvider.java @@ -1,7 +1,5 @@ package ai.timefold.solver.core.testdomain.constraintverifier; -import java.util.Set; - import ai.timefold.solver.core.api.score.HardSoftScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; @@ -22,7 +20,6 @@ public Constraint justifyWithFirstJustification(ConstraintFactory constraintFact return constraintFactory.forEach(TestdataConstraintVerifierFirstEntity.class) .penalize(HardSoftScore.ONE_HARD) .justifyWith((entity, score) -> new TestFirstJustification(entity.getCode())) - .indictWith(Set::of) .asConstraint("Justify with first justification"); } @@ -30,7 +27,7 @@ public Constraint justifyWithNoJustifications(ConstraintFactory constraintFactor return constraintFactory.forEach(TestdataConstraintVerifierFirstEntity.class) .filter(entity -> entity.getCode().equals("Should not filter")) .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Justify without justifications and indictments"); + .asConstraint("Justify without justifications"); } } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/constraintweightoverrides/TestdataConstraintWeightOverridesIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/constraintweightoverrides/TestdataConstraintWeightOverridesIncrementalScoreCalculator.java deleted file mode 100644 index a72bcf4d3e9..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/constraintweightoverrides/TestdataConstraintWeightOverridesIncrementalScoreCalculator.java +++ /dev/null @@ -1,63 +0,0 @@ -package ai.timefold.solver.core.testdomain.constraintweightoverrides; - -import java.util.ArrayList; -import java.util.List; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import ai.timefold.solver.core.testdomain.TestdataEntity; - -import org.jspecify.annotations.NonNull; - -public final class TestdataConstraintWeightOverridesIncrementalScoreCalculator - implements IncrementalScoreCalculator { - - private TestdataConstraintWeightOverridesSolution workingSolution; - private List entityList; - - @Override - public void resetWorkingSolution(@NonNull TestdataConstraintWeightOverridesSolution workingSolution) { - this.workingSolution = workingSolution; - this.entityList = new ArrayList<>(workingSolution.getEntityList()); - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - entityList.add((TestdataEntity) entity); - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - throw new UnsupportedOperationException(); // Will not be called. - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - throw new UnsupportedOperationException(); // Will not be called. - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - entityList.remove((TestdataEntity) entity); - } - - @Override - public @NonNull SimpleScore calculateScore() { - var firstWeight = workingSolution.getConstraintWeightOverrides() - .getConstraintWeight("First weight"); - if (firstWeight != null) { - return SimpleScore.of(workingSolution.getEntityList().size() * firstWeight.score()); - } - return SimpleScore.of(workingSolution.getEntityList().size()); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/TestdataPinnedListIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/TestdataPinnedListIncrementalScoreCalculator.java deleted file mode 100644 index 74541b1e5e7..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/TestdataPinnedListIncrementalScoreCalculator.java +++ /dev/null @@ -1,55 +0,0 @@ -package ai.timefold.solver.core.testdomain.list.pinned; - -import java.util.ArrayList; -import java.util.List; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; - -import org.jspecify.annotations.NonNull; - -public final class TestdataPinnedListIncrementalScoreCalculator - implements IncrementalScoreCalculator { - - private List entityList; - - @Override - public void resetWorkingSolution(@NonNull TestdataPinnedListSolution workingSolution) { - this.entityList = new ArrayList<>(workingSolution.getEntityList()); - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - entityList.add((TestdataPinnedListEntity) entity); - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // No need to do anything. - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // No need to do anything. - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - entityList.remove((TestdataPinnedListEntity) entity); - } - - @Override - public @NonNull SimpleScore calculateScore() { - return SimpleScore.of(-entityList.size()); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/index/TestdataPinnedWithIndexListCMAIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/index/TestdataPinnedWithIndexListCMAIncrementalScoreCalculator.java deleted file mode 100644 index efabfc8cacf..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/index/TestdataPinnedWithIndexListCMAIncrementalScoreCalculator.java +++ /dev/null @@ -1,108 +0,0 @@ -package ai.timefold.solver.core.testdomain.list.pinned.index; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -public class TestdataPinnedWithIndexListCMAIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - private TestdataPinnedWithIndexListSolution workingSolution; - private Map> indictmentMap; - - @Override - public void resetWorkingSolution(@NonNull TestdataPinnedWithIndexListSolution workingSolution) { - resetWorkingSolution(workingSolution, true); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataPinnedWithIndexListSolution workingSolution, - boolean constraintMatchEnabled) { - this.workingSolution = workingSolution; - this.indictmentMap = null; - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - return update().getScore(); - } - - private DefaultConstraintMatchTotal update() { - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(ConstraintRef.of("testConstraint"), SimpleScore.ONE); - this.indictmentMap = new HashMap<>(); - for (TestdataPinnedWithIndexListValue left : workingSolution.getValueList()) { - TestdataPinnedWithIndexListEntity entity = left.getEntity(); - if (entity == null) { - continue; - } - for (TestdataPinnedWithIndexListValue right : workingSolution.getValueList()) { - if (Objects.equals(right.getEntity(), entity)) { - var constraintMatch = - constraintMatchTotal.addConstraintMatch(List.of(left, right), SimpleScore.ONE.negate()); - Stream.of(left, right) - .forEach(value -> indictmentMap - .computeIfAbsent(value, key -> new DefaultIndictment<>(key, SimpleScore.ZERO)) - .getConstraintMatchSet() - .add(constraintMatch)); - } - } - } - return constraintMatchTotal; - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - return Collections.singleton(update()); - } - - @Override - public @Nullable Map> getIndictmentMap() { - update(); - return indictmentMap; - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/index/TestdataPinnedWithIndexListIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/index/TestdataPinnedWithIndexListIncrementalScoreCalculator.java deleted file mode 100644 index 2b108313e5c..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/pinned/index/TestdataPinnedWithIndexListIncrementalScoreCalculator.java +++ /dev/null @@ -1,55 +0,0 @@ -package ai.timefold.solver.core.testdomain.list.pinned.index; - -import java.util.ArrayList; -import java.util.List; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; - -import org.jspecify.annotations.NonNull; - -public final class TestdataPinnedWithIndexListIncrementalScoreCalculator - implements IncrementalScoreCalculator { - - private List entityList; - - @Override - public void resetWorkingSolution(@NonNull TestdataPinnedWithIndexListSolution workingSolution) { - this.entityList = new ArrayList<>(workingSolution.getEntityList()); - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - entityList.add((TestdataPinnedWithIndexListEntity) entity); - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // No need to do anything. - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // No need to do anything. - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - entityList.remove((TestdataPinnedWithIndexListEntity) entity); - } - - @Override - public @NonNull SimpleScore calculateScore() { - return SimpleScore.of(-entityList.size()); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/shadowhistory/TestdataListWithShadowHistoryConstraintProvider.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/shadowhistory/TestdataListWithShadowHistoryConstraintProvider.java new file mode 100644 index 00000000000..f1668343aa1 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/shadowhistory/TestdataListWithShadowHistoryConstraintProvider.java @@ -0,0 +1,24 @@ +package ai.timefold.solver.core.testdomain.list.shadowhistory; + +import ai.timefold.solver.core.api.score.SimpleScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; + +import org.jspecify.annotations.NonNull; + +public class TestdataListWithShadowHistoryConstraintProvider implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { + constraintFactory.forEach(TestdataListEntityWithShadowHistory.class) + .penalize(SimpleScore.ONE, entity -> { + var size = (long) entity.getValueList().size(); + return size * size; + }) + .asConstraint("testConstraint") + }; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/shadowhistory/TestdataListWithShadowHistoryIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/shadowhistory/TestdataListWithShadowHistoryIncrementalScoreCalculator.java deleted file mode 100644 index 4a0d100b46f..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/shadowhistory/TestdataListWithShadowHistoryIncrementalScoreCalculator.java +++ /dev/null @@ -1,91 +0,0 @@ -package ai.timefold.solver.core.testdomain.list.shadowhistory; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -public class TestdataListWithShadowHistoryIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - private TestdataListSolutionWithShadowHistory workingSolution; - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - var constraintMatchTotal = update(workingSolution); - return constraintMatchTotal.getScore(); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataListSolutionWithShadowHistory workingSolution) { - resetWorkingSolution(workingSolution, true); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataListSolutionWithShadowHistory workingSolution, - boolean constraintMatchEnabled) { - this.workingSolution = workingSolution; - } - - private DefaultConstraintMatchTotal update(TestdataListSolutionWithShadowHistory workingSolution) { - var constraintRef = ConstraintRef.of("testConstraint"); - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintRef, SimpleScore.ONE); - for (var e : workingSolution.getEntityList()) { - int value = (int) Math.pow(e.getValueList().size(), 2); - if (value != 0) { - constraintMatchTotal.addConstraintMatch(Collections.singletonList(e), SimpleScore.of(-value)); - } - } - return constraintMatchTotal; - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - var constraintMatchTotal = update(workingSolution); - return Collections.singleton(constraintMatchTotal); - } - - @Override - public @Nullable Map> getIndictmentMap() { - throw new UnsupportedOperationException(); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListIncrementalScoreCalculator.java deleted file mode 100644 index 9c967dfd6d4..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListIncrementalScoreCalculator.java +++ /dev/null @@ -1,59 +0,0 @@ -package ai.timefold.solver.core.testdomain.list.unassignedvar; - -import java.util.ArrayList; -import java.util.List; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; - -import org.jspecify.annotations.NonNull; - -public final class TestdataAllowsUnassignedValuesListIncrementalScoreCalculator - implements IncrementalScoreCalculator { - - private List entityList; - - @Override - public void resetWorkingSolution(@NonNull TestdataAllowsUnassignedValuesListSolution workingSolution) { - this.entityList = new ArrayList<>(workingSolution.getEntityList()); - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - entityList.add((TestdataAllowsUnassignedValuesListEntity) entity); - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - throw new UnsupportedOperationException(); // Will not be called. - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - throw new UnsupportedOperationException(); // Will not be called. - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // No need to do anything. - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - entityList.remove((TestdataAllowsUnassignedValuesListEntity) entity); - } - - @Override - public @NonNull SimpleScore calculateScore() { - int i = 0; - for (TestdataAllowsUnassignedValuesListEntity entity : entityList) { - i += entity.getValueList().size(); - } - return SimpleScore.of(-i); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarConstraintProvider.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarConstraintProvider.java new file mode 100644 index 00000000000..35872fbbb80 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultiVarConstraintProvider.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.core.testdomain.multivar; + +import ai.timefold.solver.core.api.score.SimpleScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; + +import org.jspecify.annotations.NonNull; + +public class TestdataMultiVarConstraintProvider implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { + constraintFactory.forEachIncludingUnassigned(TestdataMultiVarEntity.class) + .penalize(SimpleScore.ONE, entity -> { + int count = entity.getPrimaryValue() == entity.getSecondaryValue() ? 0 : 1; + count += entity.getTertiaryValueAllowedUnassigned() == null ? 0 : 1; + return count; + }) + .asConstraint("testConstraint") + }; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultivarIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultivarIncrementalScoreCalculator.java deleted file mode 100644 index 00acf109f6a..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/multivar/TestdataMultivarIncrementalScoreCalculator.java +++ /dev/null @@ -1,96 +0,0 @@ -package ai.timefold.solver.core.testdomain.multivar; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -public class TestdataMultivarIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - private TestdataMultiVarSolution workingSolution; - private Map> indictmentMap; - - @Override - public void resetWorkingSolution(@NonNull TestdataMultiVarSolution workingSolution) { - resetWorkingSolution(workingSolution, true); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataMultiVarSolution workingSolution, boolean constraintMatchEnabled) { - this.workingSolution = workingSolution; - this.indictmentMap = null; - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - return update().getScore(); - } - - private DefaultConstraintMatchTotal update() { - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(ConstraintRef.of("testConstraint"), SimpleScore.ONE); - this.indictmentMap = new HashMap<>(); - for (TestdataMultiVarEntity left : workingSolution.getMultiVarEntityList()) { - int count = left.getPrimaryValue() == left.getSecondaryValue() ? 0 : 1; - count += left.getTertiaryValueAllowedUnassigned() == null ? 0 : 1; - var constraintMatch = constraintMatchTotal.addConstraintMatch(List.of(left), SimpleScore.of(-count)); - indictmentMap.computeIfAbsent(left, key -> new DefaultIndictment<>(key, SimpleScore.ZERO)) - .getConstraintMatchSet() - .add(constraintMatch); - } - return constraintMatchTotal; - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - return Collections.singleton(update()); - } - - @Override - public @Nullable Map> getIndictmentMap() { - update(); - return indictmentMap; - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/lavish/TestdataLavishSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/lavish/TestdataLavishSolution.java index f3213b7f7ea..b2c8bf7660e 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/lavish/TestdataLavishSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/lavish/TestdataLavishSolution.java @@ -89,19 +89,19 @@ public TestdataLavishSolution(String code) { } public TestdataLavishValueGroup getFirstValueGroup() { - return valueGroupList.get(0); + return valueGroupList.getFirst(); } public TestdataLavishValue getFirstValue() { - return valueList.get(0); + return valueList.getFirst(); } public TestdataLavishEntityGroup getFirstEntityGroup() { - return entityGroupList.get(0); + return entityGroupList.getFirst(); } public TestdataLavishEntity getFirstEntity() { - return entityList.get(0); + return entityList.getFirst(); } // ************************************************************************ diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/TestdataShadowedConstraintProviderClass.java b/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/TestdataShadowedConstraintProviderClass.java new file mode 100644 index 00000000000..65b45743a21 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/TestdataShadowedConstraintProviderClass.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.core.testdomain.shadow; + +import ai.timefold.solver.core.api.score.SimpleScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.Joiners; + +import org.jspecify.annotations.NonNull; + +public final class TestdataShadowedConstraintProviderClass implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { + constraintFactory.forEach(TestdataShadowedEntity.class) + .filter(entity -> entity.getValue() != null) + .join(TestdataShadowedEntity.class, + Joiners.equal(TestdataShadowedEntity::getValue)) + .penalize(SimpleScore.ONE) + .asConstraint("testConstraint") + }; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/TestdataShadowedIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/TestdataShadowedIncrementalScoreCalculator.java deleted file mode 100644 index 604614e7666..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/TestdataShadowedIncrementalScoreCalculator.java +++ /dev/null @@ -1,108 +0,0 @@ -package ai.timefold.solver.core.testdomain.shadow; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; -import ai.timefold.solver.core.testdomain.TestdataValue; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -public class TestdataShadowedIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - private TestdataShadowedSolution workingSolution; - private Map> indictmentMap; - - @Override - public void resetWorkingSolution(@NonNull TestdataShadowedSolution workingSolution) { - resetWorkingSolution(workingSolution, true); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataShadowedSolution workingSolution, boolean constraintMatchEnabled) { - this.workingSolution = workingSolution; - this.indictmentMap = null; - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - return update().getScore(); - } - - private DefaultConstraintMatchTotal update() { - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(ConstraintRef.of("testConstraint"), SimpleScore.ONE); - this.indictmentMap = new HashMap<>(); - for (TestdataShadowedEntity left : workingSolution.getEntityList()) { - TestdataValue value = left.getValue(); - if (value == null) { - continue; - } - for (TestdataShadowedEntity right : workingSolution.getEntityList()) { - if (Objects.equals(right.getValue(), value)) { - var constraintMatch = - constraintMatchTotal.addConstraintMatch(List.of(left, right), SimpleScore.ONE.negate()); - Stream.of(left, right) - .forEach(entity -> indictmentMap - .computeIfAbsent(entity, key -> new DefaultIndictment<>(key, SimpleScore.ZERO)) - .getConstraintMatchSet() - .add(constraintMatch)); - } - } - } - return constraintMatchTotal; - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - return Collections.singleton(update()); - } - - @Override - public @Nullable Map> getIndictmentMap() { - update(); - return indictmentMap; - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedConstraintProvider.java b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedConstraintProvider.java index 40c4bf7571f..7521823f829 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedConstraintProvider.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedConstraintProvider.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.Joiners; import org.jspecify.annotations.NonNull; @@ -18,9 +19,10 @@ public final class TestdataAllowsUnassignedConstraintProvider implements Constra private Constraint valueConstraint(ConstraintFactory constraintFactory) { return constraintFactory.forEachIncludingUnassigned(TestdataAllowsUnassignedEntity.class) - .filter(entity -> entity.getValue() == null) + .join(constraintFactory.forEachIncludingUnassigned(TestdataAllowsUnassignedEntity.class), + Joiners.equal(TestdataAllowsUnassignedEntity::getValue)) .penalize(SimpleScore.ONE) - .asConstraint("Unassigned entities"); + .asConstraint("testConstraint"); } } \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedIncrementalScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedIncrementalScoreCalculator.java deleted file mode 100644 index e6e087d1a83..00000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/TestdataAllowsUnassignedIncrementalScoreCalculator.java +++ /dev/null @@ -1,106 +0,0 @@ -package ai.timefold.solver.core.testdomain.unassignedvar; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; -import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.constraint.Indictment; -import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; -import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; -import ai.timefold.solver.core.testdomain.TestdataValue; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -public class TestdataAllowsUnassignedIncrementalScoreCalculator - implements ConstraintMatchAwareIncrementalScoreCalculator { - - private TestdataAllowsUnassignedSolution workingSolution; - private Map> indictmentMap; - - @Override - public void resetWorkingSolution(@NonNull TestdataAllowsUnassignedSolution workingSolution) { - resetWorkingSolution(workingSolution, true); - } - - @Override - public void resetWorkingSolution(@NonNull TestdataAllowsUnassignedSolution workingSolution, - boolean constraintMatchEnabled) { - this.workingSolution = workingSolution; - this.indictmentMap = null; - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull SimpleScore calculateScore() { - return update().getScore(); - } - - private DefaultConstraintMatchTotal update() { - var constraintMatchTotal = new DefaultConstraintMatchTotal<>(ConstraintRef.of("testConstraint"), SimpleScore.ONE); - this.indictmentMap = new HashMap<>(); - for (TestdataAllowsUnassignedEntity left : workingSolution.getEntityList()) { - TestdataValue value = left.getValue(); - for (TestdataAllowsUnassignedEntity right : workingSolution.getEntityList()) { - if (Objects.equals(right.getValue(), value)) { - var constraintMatch = - constraintMatchTotal.addConstraintMatch(List.of(left, right), SimpleScore.ONE.negate()); - Stream.of(left, right) - .forEach(entity -> indictmentMap - .computeIfAbsent(entity, key -> new DefaultIndictment<>(key, SimpleScore.ZERO)) - .getConstraintMatchSet() - .add(constraintMatch)); - } - } - } - return constraintMatchTotal; - } - - @Override - public @NonNull Collection> getConstraintMatchTotals() { - return Collections.singleton(update()); - } - - @Override - public @Nullable Map> getIndictmentMap() { - update(); - return indictmentMap; - } -} diff --git a/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataOverconstrainedSolverConfig.xml b/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataOverconstrainedSolverConfig.xml deleted file mode 100644 index b360a8a286f..00000000000 --- a/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataOverconstrainedSolverConfig.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedSolution - ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEntity - - - - ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedIncrementalScoreCalculator - - diff --git a/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataShadowedSolverConfig.xml b/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataShadowedSolverConfig.xml deleted file mode 100644 index a2c068f875a..00000000000 --- a/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataShadowedSolverConfig.xml +++ /dev/null @@ -1,7 +0,0 @@ - - ai.timefold.solver.core.testdomain.shadow.TestdataShadowedSolution - ai.timefold.solver.core.testdomain.shadow.TestdataShadowedEntity - - ai.timefold.solver.core.testdomain.shadow.TestdataShadowedIncrementalScoreCalculator - - diff --git a/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataSolverConfig.xml b/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataSolverConfig.xml index b8e2f19f62f..4270c97d61e 100644 --- a/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataSolverConfig.xml +++ b/core/src/test/resources/ai/timefold/solver/core/api/solver/testdataSolverConfig.xml @@ -5,6 +5,6 @@ - ai.timefold.solver.core.testdomain.TestdataIncrementalScoreCalculator + ai.timefold.solver.core.testdomain.TestdataEasyScoreCalculator diff --git a/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.png b/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.png index 5eac0eb70ca..30a3d2dcf69 100644 Binary files a/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.png and b/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.png differ diff --git a/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.svg b/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.svg index 27086847850..9193f327ac7 100644 --- a/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.svg +++ b/docs/src/modules/ROOT/images/constraints-and-score/understanding-the-score/scoreVisualization.svg @@ -2,22 +2,22 @@ + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:snap-intersection-paths="true" + inkscape:showpageshadow="2" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1"> + id="grid2845" + originx="0" + originy="0" + units="px" /> @@ -1035,7 +1041,7 @@ Score visualization Score visualization + Explain the score of a solution by breaking it down. Explain the score of a solution by breaking it down. + 3 + id="flowPara3682" + style="font-size:14.4849px;line-height:1.25">3 + A + id="flowPara3694" + style="font-size:14.4849px;line-height:1.25">A + B + id="flowPara3714" + style="font-size:14.4849px;line-height:1.25">B  + X X + Y Y + 90 $ 90 $ + 100 $ 100 $ + Z Z + 200 $ 200 $ + @@ -1282,7 +1311,7 @@ D + id="flowPara3788" + style="font-size:14.4849px;line-height:1.25">D  + 2 2 + @@ -1320,7 +1354,7 @@ ry="5" /> C + id="flowPara4671" + style="font-size:14.4849px;line-height:1.25">C  + CPU CPU + Cost Cost + Score-2hard/-190soft -190soft + + id="flowPara5168" + style="font-size:15px;line-height:1.25">  + Maintenance cost: -190soft -190soft + CPU capacity: -2hard -2hard + Break down per constraint type + style="font-weight:bold;font-size:20px;line-height:1.25;text-align:start;text-anchor:start">Break down per constraint type + D D + -100soft -100soft + C C + -100soft -100soft + B B + -2hard/-90soft -90soft + A A + -2hard/-90soft -90soft + Impact per planning entity + style="font-weight:bold;font-size:20px;line-height:1.25;text-align:end;text-anchor:end" + id="flowPara5345">Impact per planning entity + ConstraintMatchTotal ConstraintMatchTotal + ConstraintMatch - - Indictment + id="flowPara3132" + style="font-size:20px;line-height:1.25">ConstraintMatch - * * + * * + * * + 1 1 + Object - - - 1 + id="flowPara4389" + style="font-size:20px;line-height:1.25">Object + * - - * justificationList justification justificationList + -100soft -100soft + -90soft -90soft + -2hard -2hard + - 0..1 + diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc index 257a4511f20..8be635c3298 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc @@ -42,11 +42,8 @@ Luckily, defining constraints in Timefold Solver is very flexible through the fo Take the time to acquaint yourself with the first three techniques. Once you understand them, formalizing most business constraints becomes straightforward. -[NOTE] -==== -Do not presume that your business knows all its score constraints in advance. +NOTE: Do not presume that your business knows all its score constraints in advance. Expect score constraints to be added, changed or removed after the first releases. -==== [#scoreConstraintSignum] @@ -418,7 +415,7 @@ There are several ways to calculate the `Score` of a solution: * **xref:constraints-and-score/score-calculation.adoc[Constraint Streams API]**: Implement each constraint as a separate Constraint Stream. Fast and scalable. -* **xref:constraints-and-score/score-calculation.adoc#incrementalScoreCalculation[Incremental score calculator]** (not recommended): +* **xref:constraints-and-score/score-calculation.adoc#incrementalScoreCalculation[Incremental score calculator]**: Implement multiple low-level methods. Fast and scalable. Very difficult to implement and maintain. @@ -428,12 +425,15 @@ Implement all constraints together in a single method. Does not scale. Does not support xref:constraints-and-score/understanding-the-score.adoc[score explanations]. +NOTE: Incremental score calculator is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. + Every score calculation type can work with any Score definition (such as ``HardSoftScore`` or ``HardMediumSoftScore``). All score calculation types are object-oriented and can reuse existing Java code. [IMPORTANT] ==== -The score calculation must be read-only. +The score calculation must be deterministic and free of side-effects. It must not change the planning entities or the problem facts in any way. For example, it must not call a setter method on a planning entity in the score calculation. @@ -444,7 +444,6 @@ As a result, there is no guarantee that changes applied during score calculation To update planning entities when the planning variable change, use xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] instead. ==== - [#initializingScoreTrend] === `InitializingScoreTrend` diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index 60885e26b1d..e9d3fa73831 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -306,7 +306,7 @@ The code on the hot path of your application needs to be as fast as possible. * Avoid using `java.util.Stream` or any other form of explicit iteration in constraints, as that is much slower. [#enableAutomaticNodeSharing] -== Enable automatic node sharing (Enterprise Edition only) +== Enable automatic node sharing If you are using the xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition], you should xref:enterprise-edition/enterprise-edition.adoc#automaticNodeSharing[enable automatic node sharing] as it can significantly speed up score calculation. [#benchmark] @@ -317,11 +317,8 @@ JVM performance may differ by as much as 20% between runs. To decide whether you == Constraint Profiling -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. xref:constraints-and-score/score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for xref:enterprise-edition/enterprise-edition.adoc#constraintProfiling[profiling constraints]. Profiling constraints allows you to identify which constraints are taking the majority of the time spent in score calculation and thus are worth optimizing. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc index 935acaf4246..574782f9aa0 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc @@ -72,7 +72,7 @@ The following figure illustrates this xref:constraints-and-score/performance.ado image::constraints-and-score/score-calculation/constraintStreamIncrementalCalculation.png[align="center"] Constraint Streams API also has advanced support for xref:constraints-and-score/understanding-the-score.adoc[score explanation] -through xref:constraintStreamsCustomizingJustificationsAndIndictments[custom justifications and indictments]. +through xref:constraintStreamsCustomizingJustifications[custom justifications]. image::constraints-and-score/score-calculation/constraintStreamJustification.png[align="center"] @@ -352,11 +352,12 @@ By replacing the keyword `penalize` by `reward` in the name of these building bl affect score in the opposite direction. +[#constraintStreamsCustomizingJustifications] [#constraintStreamsCustomizingJustificationsAndIndictments] -==== Customizing justifications and indictments +==== Customizing justifications One of important Timefold Solver features is its ability to xref:constraints-and-score/understanding-the-score.adoc[explain the score] of solutions it produced -through the use of justifications and indictments. +through the use of justifications. By default, each constraint is justified with `ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification`, and the final tuple makes up the indicted objects. For example, in the following constraint, the indicted objects will be of type `Vehicle` and an `Integer`: @@ -377,33 +378,8 @@ Java:: .asConstraint("vehicleCapacity"); } ---- - - -==== - -For the purposes of creating a xref:constraints-and-score/understanding-the-score.adoc#indictmentHeatMap[heat map], the `Vehicle` is very important, but the naked `Integer` carries no semantics. -We can remove it by providing the `indictWith(...)` method with a custom indictment mapping: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- - protected Constraint vehicleCapacity(ConstraintFactory factory) { - return factory.forEach(Customer.class) - .filter(customer -> customer.getVehicle() != null) - .groupBy(Customer::getVehicle, sum(Customer::getDemand)) - .filter((vehicle, demand) -> demand > vehicle.getCapacity()) - .penalizeLong(HardSoftScore.ONE_HARD, - (vehicle, demand) -> demand - vehicle.getCapacity()) - .indictWith((vehicle, demand) -> List.of(vehicle)) - .asConstraint("vehicleCapacity"); - } ----- ==== -The same mechanism can also be used to transform any of the indicted objects to any other object. To present the constraint matches to the user or to send them over the wire where they can be further processed, use the `justifyWith(...)` method to provide a custom constraint justification: @@ -422,7 +398,6 @@ Java:: (vehicle, demand) -> demand - vehicle.getCapacity()) .justifyWith((vehicle, demand, score) -> new VehicleDemandOveruse(vehicle, demand, score)) - .indictWith((vehicle, demand) -> List.of(vehicle)) .asConstraint("vehicleCapacity"); } ---- @@ -1880,8 +1855,8 @@ Java:: [NOTE] ==== -You can also use methods `justifiesWith(...)`, `justifiesWithExactly(...)`, `indictsWith(...)` and `indictsWithExactly(...)` -to validate the expected constraint justifications and indictments mapped by the constraint definition. +You can also use methods `justifiesWith(...)` and `justifiesWithExactly(...)` +to validate the expected constraint justifications mapped by the constraint definition. ==== Finally, the `penalizesBy(...)` call completes the test, @@ -1897,7 +1872,7 @@ If the tested constraints depend on shadow variables, it is your responsibility to either assign the correct values beforehand, or to call `settingAllShadowVariables()` to have them automatically updated. -When executing `justifiesWith(...)`, `justifiesWithExactly(...)`, `indictsWith(...)` and `indictsWithExactly(...)`, +When executing `justifiesWith(...)` and `justifiesWithExactly(...)`, comparisons are made using the standard method `equals` on the fact problem instances. [#constraintStreamsTestingAllConstraints] @@ -2058,17 +2033,21 @@ add the `easyScoreCalculatorCustomProperties` element and use xref:using-timefol [#incrementalScoreCalculation] === Incremental score calculation -A way to implement your score calculation incrementally in a supported programming language. +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. -* Advantages: -** Very fast and scalable; currently the fastest if implemented correctly. -* Disadvantages: -** Hard to write -*** A scalable implementation heavily uses maps, indexes etc. +A way to implement your score calculation incrementally in Java. + +Advantages:: +- Very fast and scalable; possibly an order of magnitude faster than <>,if implemented correctly. +Disadvantages:: +- Difficult to write; a scalable implementation heavily uses maps, indexes etc. You have to learn, design, write and improve all these performance optimizations yourself. -Why not have <> do the hard work for you? -** Hard to read -*** Regular score constraint changes can lead to high maintenance costs. +- Difficult to maintain; regular constraint changes can lead to high maintenance costs. + +Considering the significant development and maintenance effort required to implement an incremental score calculator, +we recommend using <> unless its incremental performance characteristics doesn't match your needs. +This is unlikely for any but the biggest of problems. To start using an Incremental score calculator, implement all the methods of the interface `IncrementalScoreCalculator`: @@ -2115,7 +2094,7 @@ Configure it in the solver configuration: [IMPORTANT] ==== Incremental score calculator code can be difficult to write and to review. -xref:constraints-and-score/overview.adoc#invalidScoreDetection[Assert its correctness] by using an ``EasyScoreCalculator`` to fulfill +xref:constraints-and-score/overview.adoc#invalidScoreDetection[Assert its correctness] by using <> to fulfill the assertions triggered by the ``environmentMode``. ==== @@ -2134,26 +2113,56 @@ add the `incrementalScoreCalculatorCustomProperties` element and use xref:using- ---- -[#constraintMatchAwareIncrementalScoreCalculator] -==== `ConstraintMatchAwareIncrementalScoreCalculator` +[#analyzableIncrementalScoreCalculator] +==== Incremental score calculator and score analysis To add support for xref:constraints-and-score/understanding-the-score.adoc[score analysis], -optionally also implement the `ConstraintMatchAwareIncrementalScoreCalculator` interface: +implement the `AnalyzableIncrementalScoreCalculator` interface instead of `IncrementalScoreCalculator`: + +[source,java,options="nowrap"] +---- +public interface AnalyzableIncrementalScoreCalculator> + extends IncrementalScoreCalculator { + + void enableConstraintMatch(ConstraintMatchRegistry constraintMatchRegistry); + +} +---- + +The solver will call this method exactly once, +letting your implementation know that it should start tracking constraint matches. +The `ConstraintMatchRegistry` instance passed to this method provides the API for tracking constraint matches: [source,java,options="nowrap"] ---- -public interface ConstraintMatchAwareIncrementalScoreCalculator> { +public interface ConstraintMatchRegistry> { - void resetWorkingSolution(Solution_ workingSolution, boolean constraintMatchEnabled); + ConstraintMatchRegistration registerConstraintMatch(ConstraintRef constraintRef, Score_ score, + ConstraintJustification justification); - Collection> getConstraintMatchTotals(); + ... + +} +---- + +You can notify the solver of a new constraint match by calling `registerConstraintMatch()`. +This method takes three parameters: +- `constraintRef` is a reference to the constraint which is being matched. +- `score` is the score impact of the match. +- `justification` is an object which explains the reason for the match. It is an optional argument. + +The method will return a `ConstraintMatchRegistration` instance, which provides the API for updating and retracting the match: - Map> getIndictmentMap(); +[source,java,options="nowrap"] +---- +public interface ConstraintMatchRegistration> { + + void cancel(); + + ... } ---- -Typically it means creating one `ConstraintMatchTotal` per constraint type -and calling `addConstraintMatch()` for each constraint match. -`getConstraintMatchTotals()` code often duplicates some logic of the normal `IncrementalScoreCalculator` methods. -Constraint Streams doesn't have this disadvantage, because they are aware of constraint matches by design, -without any extra domain-specific code. +By calling `cancel()`, you can notify the solver that a previously registered match is no longer valid. +Once `resetWorkingSolution()` is called on the incremental score calculator, +all registered matches are automatically canceled. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc index 11006ccafbf..752b1737310 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc @@ -8,7 +8,10 @@ The score in its pure form is just a number, and does not help us understand the It doesn't say which constraints are broken, or what caused them to break. To understand the score, it needs to be broken down. -The easiest way to do that during development is to print the score summary: +NOTE: Explainability is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. + +The easiest way to break down the score during development is to print the score summary: [tabs] ==== @@ -34,8 +37,10 @@ Explanation of score (0hard/-10343soft): ... ---- -**Note:** -The number of matches shown per constraint can be controlled by calling `analyze(vehicleRoutePlan).summarize(topLimit)`, where `topLimit` is the maximum number of matches to display for each constraint. For example, `summarize(5)` will show up to 5 matches per constraint. To show all matches use `Integer.MAX_VALUE` as parameter. +The number of matches shown per constraint can be controlled by calling `analyze(vehicleRoutePlan).summarize(topLimit)`, +where `topLimit` is the maximum number of matches to display for each constraint. +For example, `summarize(5)` will show up to 5 matches per constraint. +To show all matches use `Integer.MAX_VALUE` as parameter. [IMPORTANT] ==== @@ -49,15 +54,12 @@ In the string above, there are some previously unexplained concepts. * A _Constraint match_ is created every time a constraint causes a change to the score. * _Justifications_ are user-defined objects that implement the `ai.timefold.solver.core.api.score.stream.ConstraintJustification` interface, which carry meaningful information about a constraint match, such as its name and any metadata that the user chooses to expose. Justifications are most easily available via <>. -* _Indicted objects_ are objects which were directly involved in causing a constraint to match. -For example, if your constraints penalize each vehicle, then there will be one `ai.timefold.solver.core.api.score.constraint.Indictment` instance per vehicle, carrying the vehicle as an indicted object. -Indictments are typically used for xref:indictmentHeatMap[heat map visualization]. [NOTE] ==== xref:constraints-and-score/score-calculation.adoc[Constraint Streams API] can analyze the score natively. xref:constraints-and-score/score-calculation.adoc#incrementalScoreCalculation[Incremental Java score calculation] requires -xref:constraints-and-score/score-calculation.adoc#constraintMatchAwareIncrementalScoreCalculator[implementing an extra interface]. +xref:constraints-and-score/score-calculation.adoc#analyzableIncrementalScoreCalculator[implementing an extra interface]. xref:constraints-and-score/score-calculation.adoc#easyScoreCalculation[Easy Java score calculation] does not support score explanation. ==== @@ -158,7 +160,7 @@ constraintAnalysis.matches().forEach(matchAnalysis -> { ==== Each match is accompanied by the score difference it caused, and a justification object (see above). -Typically, the scoring engine creates justification objects automatically by using the results of xref:constraints-and-score/score-calculation.adoc#constraintStreamsCustomizingJustificationsAndIndictments[Constraint Streams' `justifyWith(...)` call]. +Typically, the scoring engine creates justification objects automatically by using the results of xref:constraints-and-score/score-calculation.adoc#constraintStreamsCustomizingJustifications[Constraint Streams' `justifyWith(...)` call]. [#scoreAnalysisDiff] === Identifying changes between two solutions @@ -228,11 +230,9 @@ In that case, use `ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT` instead of the de [#solutionDiff] == Solution Diff: What changed between now and then? -[IMPORTANT] -==== -The solution diff is available as a xref:upgrading-timefold-solver/backwards-compatibility.adoc#previewFeatures[preview feature]. +IMPORTANT: The solution diff is exclusive to Timefold Solver Enterprise Edition. +It is also only available as a xref:upgrading-timefold-solver/backwards-compatibility.adoc#previewFeatures[preview feature]. It may be subject to change and must be enabled in the solver configuration by setting: `PLANNING_SOLUTION_DIFF` -==== Using the `SolutionManager` API, you can compare two solutions provided by the solver, and find out what changed between them: @@ -261,45 +261,4 @@ as well as the old and new values of the changed variables. It also has a useful `toString()` for a quick overview of the changes. Do not attempt to parse this string or expose it in services, its format is not stable and is subject to change. - -[#indictmentHeatMap] -== Heat map: Visualize the hot planning entities - -To show a heat map in the UI that highlights the planning entities and problem facts have an impact on the ``Score``, get the `Indictment` map from the ``ScoreExplanation``: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -SolutionManager solutionManager = SolutionManager.create(solverFactory); -ScoreExplanation scoreExplanation = solutionManager.explain(vehicleRoutePlan); -Map> indictmentMap = scoreExplanation.getIndictmentMap(); -for (Visit visit : vehicleRoutePlan.getVisits()) { - Indictment indictment = indictmentMap.get(visit); - if (indictment == null) { - continue; - } - // The score impact of that planning entity - HardSoftScore totalScore = indictment.getScore(); - - for (ConstraintMatch constraintMatch : indictment.getConstraintMatchSet()) { - String constraintName = constraintMatch.getConstraintName(); - HardSoftScore score = constraintMatch.getScore(); - ... - } -} ----- -==== - -[NOTE] -==== -`ScoreExplanation` should only be used for processing indictments. -For analyzing the score and processing constraint matches, use <> instead, which is faster and JSON-friendly. -==== - -Each `Indictment` is the sum of all constraints where that justification object is involved with. -The sum of all the `Indictment.getScoreTotal()` differs from the overall score, because multiple ``Indictment``s can share the same `ConstraintMatch`. - image::constraints-and-score/understanding-the-score/scoreVisualization.png[align="center"] diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc index 6076fdab2dc..465b3095a19 100644 --- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc +++ b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc @@ -1,18 +1,17 @@ -= Enterprise Edition += Timefold Solver Enterprise Edition :page-aliases: partitioned-search/partitioned-search.adoc :doctype: book :sectnums: :icons: font -Timefold Solver Enterprise Edition is a commercial product that offers additional features, +_Timefold Solver Enterprise Edition_ is a commercial product that offers <>, such as <> and <>. -These features are essential to scale out to huge datasets. +These features are essential to scale out to large datasets. -Unlike Timefold Solver Community Edition, the Enterprise Edition is not open-source. +Unlike _Timefold Solver Community Edition_, the _Timefold Solver Enterprise Edition_ is not open-source. We offer free licenses to non-profit organizations and for academic research. -Everyone else may be entitled to a trial license to evaluate the product before purchasing. -Please https://timefold.ai/contact[contact Timefold] -to obtain an Enterprise license. +Others may be entitled to a trial license to evaluate the product before purchasing. +Please https://timefold.ai/contact[contact Timefold] to get your Enterprise license. TIP: Looking for quicker time-to-value? Timefold offers https://docs.timefold.ai/[pre-built, fully tuned optimization models], no constraint building required. @@ -24,8 +23,8 @@ see https://timefold.ai/pricing[Timefold Pricing]. [#switchToEnterpriseEdition] == Switch to Enterprise Edition -To switch from Timefold Solver Community Edition to Enterprise Edition, -https://timefold.ai/contact[contact us first] to obtain an Enterprise License. +To switch from the open source _Timefold Solver Community Edition_ to _Enterprise Edition_, +https://timefold.ai/contact[contact us first] to get your Enterprise License. This license is represented by a file named `timefold-enterprise-license.pem`, which you can introduce to your project by one of the following methods: @@ -34,11 +33,15 @@ which you can introduce to your project by one of the following methods: * Place the file in the user's home directory. * Place the file in the root of your application classpath. -Having done that, replace references to Community Edition artifacts by their Enterprise Edition counterparts +Take care not to leak the license file, for example by committing it to a public repository, +logging the license file content in logs, or sharing it with unauthorized parties. + +Once your license is in place, +replace references to open-source artifacts by their enterprise counterparts as shown in the following table: |=== -|Community Edition|Enterprise Edition +|Open source|Enterprise |`ai.timefold.solver:timefold-solver-bom` |`ai.timefold.solver.enterprise:timefold-solver-enterprise-bom` @@ -46,34 +49,45 @@ as shown in the following table: |`ai.timefold.solver:timefold-solver-core` |`ai.timefold.solver.enterprise:timefold-solver-enterprise-core` +|`ai.timefold.solver:timefold-solver-jackson` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-jackson` + |`ai.timefold.solver:timefold-solver-quarkus` |`ai.timefold.solver.enterprise:timefold-solver-enterprise-quarkus` +|`ai.timefold.solver:timefold-solver-quarkus-jackson` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-quarkus-jackson` + |`ai.timefold.solver:timefold-solver-spring-boot-starter` |`ai.timefold.solver.enterprise:timefold-solver-enterprise-spring-boot-starter` |=== +If you were not using some of these open source artifacts until now, +you do not need to add the enterprise counterparts to your project either. + [#enterpriseEditionFeatures] -== Features of Enterprise Edition +== Features included in Enterprise Edition -The following features are only available in Timefold Solver Enterprise Edition: +The following features are only available in _Timefold Solver Enterprise Edition_: -* <>, -* <>, -* <>, -* <>, -* and <>. +* xref:constraints-and-score/understanding-the-score.adoc[Explainability], +* <> for faster and better results, +* <> and <>, +* <> for speeding up Constraint Streams, +* xref:constraints-and-score/score-calculation.adoc#incrementalScoreCalculation[incremental score calculator] for the fastest possible move evaluation, +* and <> to not overload the consumers such as messaging queues. +We also automatically enable performance optimizations not available in the open-source version, +leading to significantly improved performance for some use cases, such as: + +- Heavy use of xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variables]. [#nearbySelection] === Nearby selection -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. In some use cases (such as TSP and VRP, but also in other cases), changing entities to nearby values or swapping nearby entities leads to better results faster. @@ -277,8 +291,8 @@ only set the distribution type (so without a `distributionSizeMaximum` parameter === Multi-threaded solving Multi-threaded solving is a term -which encapsulates several features that allow Timefold Solver to run on multi-core machines. -Timefold Solver Enterprise Edition makes multi-threaded solving more powerful by introducing +which encapsulates several features that allow Timefold Solver to run on multi-core machines, +such as <> and <>. @@ -287,11 +301,8 @@ For a primer on multi-threaded solving in general, see xref:using-timefold-solve [#multithreadedIncrementalSolving] ==== Multi-threaded incremental solving -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. With this feature, the solver can run significantly faster, getting you the right solution earlier. @@ -436,11 +447,8 @@ use `threadFactoryClass` to plug in a <> with it: [#automaticNodeSharing] === Automatic node sharing -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. When a `ConstraintProvider` does an operation for multiple constraints (such as finding all shifts corresponding to an employee), that work can be shared. This can significantly improve move evaluation speed if the repeated operation is computationally expensive: @@ -850,11 +855,8 @@ From the above, you can see how this feature allows building blocks to share fun [#constraintProfiling] === Constraint Profiling -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. Profiling allows you to identify which constraints are taking the majority of time spent in score calculation and thus are worth optimizing. Unfortunately, traditional profiling tools only report the internal classes used to calculate constraints instead of the constraints themselves. xref:constraints-and-score/score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for profiling constraints directly. @@ -1020,11 +1022,8 @@ and average the results. [#throttlingBestSolutionEvents] === Throttling best solution events in `SolverManager` -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. This feature helps you avoid overloading your system with best solution events, especially in the early phase of the solving process when the solver is typically improving the solution very rapidly. @@ -1081,11 +1080,8 @@ both these consumers will receive the final best solution. [#multistageMoves] === Multistage Moves -[NOTE] -==== -This feature is a commercial feature of Timefold Solver Enterprise Edition. -It is not available in the Community Edition. -==== +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. +It is not available in the open source version. Multistage moves are moves composed of one or more stages, where each stage selects a single `Move` to execute. diff --git a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc index 6ebc535b32f..72fa7fbf08c 100644 --- a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc +++ b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc @@ -4,20 +4,20 @@ == How is Timefold Solver Licensed? -Timefold Solver Community Edition is _open source_ software, +Timefold Solver is _open source_ software, released under http://www.apache.org/licenses/LICENSE-2.0.html[the Apache License 2.0]. This license is very liberal and allows reuse for commercial purposes. Read http://www.apache.org/foundation/licence-FAQ.html#WhatDoesItMEAN[the layman's explanation]. -Timefold Solver Enterprise Edition is a commercial product -that offers xref:enterprise-edition/enterprise-edition.adoc#enterpriseEditionFeatures[additional features] +_Timefold Solver Enterprise Edition_ is a commercial product +that offers xref:enterprise-edition/enterprise-edition.adoc#features[additional features] to scale out to very large datasets. To find out more, see xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition section] of this documentation. == Does Timefold offer pre-built models? Timefold offers a suite of pre-built PlanningAI models designed to expedite development by addressing complex scheduling and routing challenges across various industries. -These models are built upon Timefold Solver Enterprise Edition technology and are accessible through a REST API, facilitating seamless integration into your applications. +These models are built upon _Timefold Solver Enterprise Edition_ technology and are accessible through a REST API, facilitating seamless integration into your applications. See all available models on https://app.timefold.ai/[our platform]. diff --git a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc index 4179d64dbf7..772873ccfce 100644 --- a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc +++ b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc @@ -38,11 +38,8 @@ This is often useful for xref:responding-to-change/responding-to-change.adoc#rea Defaults to `false`. {property_prefix}timefold.solver.{solver_name_prefix}move-thread-count:: -Enable multi-threaded solving for a single problem, which increases CPU consumption. +Enable xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multithreaded incremental solving] for a single problem, which increases CPU consumption. Defaults to `NONE`. -Note that this is a feature of the xref:enterprise-edition/enterprise-edition.adoc[Enterprise edition], -which is Timefold's commercial offering. -See xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multithreaded incremental solving]. {property_prefix}timefold.solver.{solver_name_prefix}nearby-distance-meter-class:: Enable the xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[Nearby Selection] quick configuration. diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc index 34c30afad0f..e9fe382b40d 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc @@ -803,13 +803,11 @@ Mimic selection is useful to create <` packages merged into `ai.timefold.solver.jpa.api.score`. -* Jackson score serializers: per-score-type packages merged into `ai.timefold.solver.jackson.api.score.buildin`. +* Jackson score serializers: per-score-type packages merged into `ai.timefold.solver.jackson.api.score`. * JAXB score adapters: per-score-type packages merged into `ai.timefold.solver.jaxb.api.score`. * Quarkus Jackson score types: per-score-type packages merged into `ai.timefold.solver.quarkus.jackson.score`. * Persistence common solution API moved from `ai.timefold.solver.persistence.common.api.domain.solution` to @@ -295,6 +295,61 @@ We invite you to read the xref:upgrading-timefold-solver/migration-guides/variab ''' +.icon:exclamation-triangle[role=red] Score analysis no longer open-source +[%collapsible%open] +==== +`ScoreExplanation` has been removed. +Use `ScoreAnalysis` instead, which is now provided exclusively by _Timefold Solver Enterprise Edition_. + +`ScoreAnalysis` provides the following methods: + +* `score()` — the score of the solution +* `isSolutionInitialized()` — whether the solution is fully initialized +* `constraintAnalyses()` — list of `ConstraintAnalysis` objects, one per constraint +* `summarize()` and `summarize(int)` — human-readable score breakdown +* `getConstraintAnalysis(ConstraintRef)` and `getConstraintAnalysis(String, String)` — look up a specific constraint +* `diff(ScoreAnalysis)` — compare two score analyses + +The following `ScoreExplanation` methods have no direct replacement and must be removed manually: + +* `getSolution()` — no replacement +* `getJustificationList()` — no replacement; use `constraintAnalyses()` and filter `ConstraintAnalysis.matches()` by justification type +* `getSummary()` — use `ScoreAnalysis.summarize()` instead +* `getConstraintMatchTotalMap()` — use `ScoreAnalysis.constraintAnalyses()` instead + +Add the Timefold Solver Enterprise dependency to your project to use `ScoreAnalysis`. +There is no automated migration recipe for `ScoreExplanation`; all changes must be made manually. +==== + +''' + +.icon:exclamation-triangle[role=red] Recommended assignment no longer open-source +[%collapsible%open] +==== +The recommended assignment API is unchanged, but it now requires Timefold Solver Enterprise Edition on the classpath. + +If you use Jackson serialization, register `TimefoldEnterpriseJacksonModule` in addition to the standard module. +==== + +''' + +.icon:exclamation-triangle[role=red] Incremental score calculator no longer open-source +[%collapsible%open] +==== +`IncrementalScoreCalculator` has moved to Timefold Solver Enterprise Edition. +`ConstraintMatchAwareIncrementalScoreCalculator` was removed. +The replacement `AnalyzableIncrementalScoreCalculator` is provided by the Enterprise Edition. + +Instead of overriding methods to return constraint matches, implement the `enableConstraintMatch(ConstraintMatchRegistry)` method. +Use `ConstraintMatchRegistry.registerConstraintMatch(ConstraintRef, score, justification)` to register each match; +it returns a `ConstraintMatchRegistration` whose `cancel()` method removes the match when the assignment changes. + +Add the Timefold Solver Enterprise dependency and rewrite the implementation body manually; +there is no automated migration recipe for this change. +==== + +''' + .icon:exclamation-triangle[role=red] `Move` interface refactored [%collapsible%open] ==== @@ -310,6 +365,14 @@ which we believe is well worth the breaking change. ''' +.icon:info-circle[role=yellow] Planning solution diff no longer open-source +[%collapsible%open] +==== +The solution diff API is unchanged, but it now requires Timefold Solver Enterprise Edition on the classpath. +==== + +''' + .icon:info-circle[role=yellow] Full modularization under JPMS [%collapsible%open] ==== @@ -408,6 +471,24 @@ After in `*Solution.java`: ''' +.icon:info-circle[role=yellow] `Indictment` removed +[%collapsible%closed] +==== +The `Indictment` interface has been removed entirely, along with: + +* `indictWith()` on `UniConstraintBuilder`, `BiConstraintBuilder`, `TriConstraintBuilder`, and `QuadConstraintBuilder` +* `getIndictmentMap()` on `ScoreExplanation` + +There is no replacement type for `Indictment`. +Remove all field and variable declarations of type `Indictment`. + +To analyze per-entity score impact, use `ScoreAnalysis.constraintAnalyses()` and filter `ConstraintAnalysis.matches()` by justification. + +The OpenRewrite migration recipe automatically removes `indictWith()` call sites. +==== + +''' + .icon:info-circle[role=yellow] `@PlanningEntity` `pinningFilter` removed [%collapsible%open] ==== @@ -918,4 +999,4 @@ or, better yet, start using a collection-based value range if possible. `BestSolutionChangedEvent` has been converted from a class to an interface. Its public constructors were already deprecated; if your code constructed instances of this class (e.g. for unit tests), you must remove those usages. -==== \ No newline at end of file +==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc index 5dfb44766ac..17f86601029 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc @@ -1067,11 +1067,8 @@ for example when running benchmarks on an application server or a cloud platform ---- -[NOTE] -==== -This feature is independent of xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] (an enterprise feature), -and can be used in Timefold Solver Community Edition as well. -==== +NOTE: This feature is independent of xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] +and can be used in the open source version of Timefold Solver. [#statisticalBenchmarking] diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 2c721153098..c0835588304 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -84,11 +84,11 @@ There are several ways of running the solver in parallel: * *xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[Multi-threaded incremental solving]*: Solve 1 dataset with multiple threads without sacrificing xref:constraints-and-score/performance.adoc#incrementalScoreCalculationPerformance[incremental score calculation]. -This is an exclusive feature of the xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition]. +This is exclusively available in the xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition] of Timefold Solver. * *xref:enterprise-edition/enterprise-edition.adoc#partitionedSearch[Partitioned search]*: Split 1 dataset in multiple parts and solve them independently. -This is an exclusive feature of the xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition]. +This is exclusively available in the xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition] of Timefold Solver. * *Multi bet solving*: solve 1 dataset with multiple, isolated solvers and take the best result. ** Not recommended: This is a marginal gain for a high cost of hardware resources. ** Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] during development to determine the algorithm that is the most appropriate on average. @@ -740,7 +740,7 @@ The final best solution consumer, which is called at the end of the solving process, can be set using `withFinalBestSolutionEventConsumer(...)`. Additionally, -an improved solution consumer capable of throttling events is available in the xref:enterprise-edition/enterprise-edition.adoc#throttlingBestSolutionEvents[Enterprise Edition]. +an improved solution consumer capable of throttling events is available in the xref:enterprise-edition/enterprise-edition.adoc#throttlingBestSolutionEvents[Enterprise Edition] of the Timefold Solver. [WARNING] ==== @@ -861,5 +861,5 @@ Users of our xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition] may use the xref:enterprise-edition/enterprise-edition.adoc#throttlingBestSolutionEvents[throttling feature] to limit the number of best solution events fired over any period of time. -Community Edition users may implement their own throttling mechanism within the `Consumer` itself. +Open-source users may implement their own throttling mechanism within the `Consumer` itself. ==== diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/TimefoldJacksonModule.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/TimefoldJacksonModule.java index 605a10c5e8a..1585b302fff 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/TimefoldJacksonModule.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/TimefoldJacksonModule.java @@ -10,38 +10,31 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.common.Break; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.common.Sequence; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; import ai.timefold.solver.core.impl.domain.solution.DefaultConstraintWeightOverrides; -import ai.timefold.solver.core.impl.solver.DefaultRecommendedAssignment; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; import ai.timefold.solver.jackson.api.domain.solution.ConstraintWeightOverridesSerializer; +import ai.timefold.solver.jackson.api.score.BendableBigDecimalScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.BendableBigDecimalScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.BendableScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.BendableScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.HardMediumSoftBigDecimalScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.HardMediumSoftBigDecimalScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.HardMediumSoftScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.HardMediumSoftScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.HardSoftBigDecimalScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.HardSoftBigDecimalScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.HardSoftScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.HardSoftScoreJacksonSerializer; import ai.timefold.solver.jackson.api.score.PolymorphicScoreJacksonDeserializer; import ai.timefold.solver.jackson.api.score.PolymorphicScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.analysis.ScoreAnalysisJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.BendableBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.BendableBigDecimalScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.BendableScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.BendableScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftBigDecimalScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftBigDecimalScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleBigDecimalScoreJacksonSerializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.SimpleBigDecimalScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.SimpleBigDecimalScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.SimpleScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.SimpleScoreJacksonSerializer; import ai.timefold.solver.jackson.api.score.constraint.ConstraintRefJacksonDeserializer; import ai.timefold.solver.jackson.api.score.constraint.ConstraintRefJacksonSerializer; import ai.timefold.solver.jackson.api.score.stream.common.BreakJacksonDeserializer; @@ -52,14 +45,9 @@ import ai.timefold.solver.jackson.api.score.stream.common.SequenceChainJacksonSerializer; import ai.timefold.solver.jackson.api.score.stream.common.SequenceJacksonDeserializer; import ai.timefold.solver.jackson.api.score.stream.common.SequenceJacksonSerializer; -import ai.timefold.solver.jackson.api.solver.RecommendedAssignmentJacksonSerializer; import ai.timefold.solver.jackson.impl.domain.solution.JacksonSolutionFileIO; -import ai.timefold.solver.jackson.preview.api.domain.solution.diff.PlanningEntityDiffJacksonSerializer; -import ai.timefold.solver.jackson.preview.api.domain.solution.diff.PlanningSolutionDiffJacksonSerializer; -import ai.timefold.solver.jackson.preview.api.domain.solution.diff.PlanningVariableDiffJacksonSerializer; import tools.jackson.databind.JacksonModule; -import tools.jackson.databind.ValueSerializer; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.module.SimpleModule; @@ -80,9 +68,13 @@ public static JacksonModule createModule() { } - @SuppressWarnings({ "rawtypes", "unchecked" }) public TimefoldJacksonModule() { - super("Timefold"); + this("Timefold"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TimefoldJacksonModule(String name) { + super(name); // For non-subtype Score fields/properties, we also need to record the score type addSerializer(Score.class, new PolymorphicScoreJacksonSerializer()); addDeserializer(Score.class, new PolymorphicScoreJacksonDeserializer()); @@ -104,15 +96,9 @@ public TimefoldJacksonModule() { addSerializer(BendableBigDecimalScore.class, new BendableBigDecimalScoreJacksonSerializer()); addDeserializer(BendableBigDecimalScore.class, new BendableBigDecimalScoreJacksonDeserializer()); - // Score analysis + // Constraint weights addSerializer(ConstraintRef.class, new ConstraintRefJacksonSerializer()); addDeserializer(ConstraintRef.class, new ConstraintRefJacksonDeserializer()); - addSerializer(ScoreAnalysis.class, new ScoreAnalysisJacksonSerializer()); - var serializer = (ValueSerializer) new RecommendedAssignmentJacksonSerializer<>(); - addSerializer(RecommendedAssignment.class, serializer); - addSerializer(DefaultRecommendedAssignment.class, serializer); - - // Constraint weights addSerializer(ConstraintWeightOverrides.class, new ConstraintWeightOverridesSerializer()); addSerializer(DefaultConstraintWeightOverrides.class, new ConstraintWeightOverridesSerializer()); @@ -125,11 +111,6 @@ public TimefoldJacksonModule() { addDeserializer(SequenceChain.class, new SequenceChainJacksonDeserializer<>()); addSerializer(LoadBalance.class, new LoadBalanceJacksonSerializer()); addDeserializer(LoadBalance.class, new LoadBalanceJacksonDeserializer<>()); - - // Solution diff - addSerializer(PlanningSolutionDiff.class, new PlanningSolutionDiffJacksonSerializer()); - addSerializer(PlanningEntityDiff.class, new PlanningEntityDiffJacksonSerializer()); - addSerializer(PlanningVariableDiff.class, new PlanningVariableDiffJacksonSerializer()); } } diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableBigDecimalScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonDeserializer.java similarity index 81% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableBigDecimalScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonDeserializer.java index 42ce4d2b9e5..9c53a6df22d 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableBigDecimalScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.BendableBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableBigDecimalScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonSerializer.java similarity index 59% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableBigDecimalScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonSerializer.java index e70944c3e80..4ddeb4716f1 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableBigDecimalScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.BendableBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class BendableBigDecimalScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonDeserializer.java similarity index 79% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonDeserializer.java index f7211df538d..0a331354e69 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.BendableScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonSerializer.java similarity index 55% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonSerializer.java index ae95e5a6dc3..f3d8f7b868d 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/BendableScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.BendableScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class BendableScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftBigDecimalScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonDeserializer.java similarity index 82% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftBigDecimalScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonDeserializer.java index d059e3c8f3a..fd37f9ce376 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftBigDecimalScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardMediumSoftBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftBigDecimalScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonSerializer.java similarity index 62% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftBigDecimalScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonSerializer.java index 8c558dbb4a8..d2a6e9b05ae 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftBigDecimalScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardMediumSoftBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class HardMediumSoftBigDecimalScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonDeserializer.java similarity index 80% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonDeserializer.java index 51e5970645d..56745a0eb5e 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardMediumSoftScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonSerializer.java similarity index 57% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonSerializer.java index 7649ac921e5..9997b618768 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardMediumSoftScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardMediumSoftScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class HardMediumSoftScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftBigDecimalScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonDeserializer.java similarity index 81% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftBigDecimalScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonDeserializer.java index 202ffd99653..95aa1c39f9e 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftBigDecimalScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardSoftBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftBigDecimalScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonSerializer.java similarity index 59% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftBigDecimalScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonSerializer.java index 92bd50ae10b..08d31d762af 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftBigDecimalScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardSoftBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class HardSoftBigDecimalScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonDeserializer.java similarity index 79% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonDeserializer.java index 11ba559e6cb..b78852c7664 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardSoftScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonSerializer.java similarity index 55% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonSerializer.java index 3e6ff88d7e3..9c0ec3819fa 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/HardSoftScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardSoftScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class HardSoftScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonDeserializer.java index 44b05e9d08a..e780b58310c 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonDeserializer.java @@ -9,7 +9,6 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonSerializer.java index c30a6fe90c1..dd4ed004add 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/PolymorphicScoreJacksonSerializer.java @@ -2,7 +2,6 @@ import ai.timefold.solver.core.api.score.HardSoftScore; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonSerializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonGenerator; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleBigDecimalScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonDeserializer.java similarity index 80% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleBigDecimalScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonDeserializer.java index 7d5ed78942f..7f31afd9681 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleBigDecimalScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleBigDecimalScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonSerializer.java similarity index 58% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleBigDecimalScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonSerializer.java index 1e2b0684140..fbb3c1ffe60 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleBigDecimalScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class SimpleBigDecimalScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleScoreJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonDeserializer.java similarity index 78% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleScoreJacksonDeserializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonDeserializer.java index a4938e9a985..a5f4e8fa724 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleScoreJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonDeserializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleScoreJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonSerializer.java similarity index 54% rename from persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleScoreJacksonSerializer.java rename to persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonSerializer.java index fb85a7df4cb..d16d24f09a9 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/buildin/SimpleScoreJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonSerializer.java @@ -1,7 +1,6 @@ -package ai.timefold.solver.jackson.api.score.buildin; +package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonSerializer; public class SimpleScoreJacksonSerializer extends AbstractScoreJacksonSerializer { diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java deleted file mode 100644 index 540af381c8a..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java +++ /dev/null @@ -1,115 +0,0 @@ -package ai.timefold.solver.jackson.api.score.analysis; - -import java.util.ArrayList; -import java.util.HashMap; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; -import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.Constraint; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.ConstraintProvider; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonParser; -import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ValueDeserializer; - -/** - * Extend this to implement {@link ScoreAnalysis} deserialization specific for your domain. - * - * @param - */ -public abstract class AbstractScoreAnalysisJacksonDeserializer> - extends ValueDeserializer> { - - @Override - public final ScoreAnalysis deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { - JsonNode node = p.readValueAsTree(); - var score = parseScore(node.get("score").asString()); - var initialized = node.get("initialized").asBoolean(); - var constraintAnalysisList = new HashMap>(); - for (var constraintNode : node.get("constraints")) { - var constraintName = constraintNode.get("name").asString(); - var constraintRef = ConstraintRef.of(constraintName); - var constraintWeight = parseScore(constraintNode.get("weight").asString()); - var constraintScore = parseScore(constraintNode.get("score").asString()); - var matchScoreList = new ArrayList>(); - var matchesNode = constraintNode.get("matches"); - var matchCountNode = constraintNode.get("matchCount"); - if (matchesNode == null) { - constraintAnalysisList.put(constraintRef, - new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, null, - matchCountNode == null ? -1 : matchCountNode.asInt(-1))); - } else { - for (var matchNode : constraintNode.get("matches")) { - var matchScore = parseScore(matchNode.get("score").asString()); - var justificationNode = matchNode.get("justification"); - if (justificationNode == null) { - // Not allowed; if matches are present, they must have justifications. - throw new IllegalStateException("The match justification of constraint (%s)'s match is missing." - .formatted(constraintRef)); - } - var justificationString = justificationNode.toString(); - if (getConstraintJustificationClass(constraintRef) == null) { // String-based fallback. - var parsedJustification = parseConstraintJustification(constraintRef, justificationString, matchScore); - matchScoreList.add(new MatchAnalysis<>(constraintRef, matchScore, parsedJustification)); - } else { // Deserializer-based method. - var parsedJustification = - ctxt.readTreeAsValue(justificationNode, getConstraintJustificationClass(constraintRef)); - matchScoreList.add(new MatchAnalysis<>(constraintRef, matchScore, parsedJustification)); - } - } - constraintAnalysisList.put(constraintRef, - new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, matchScoreList)); - } - } - return new ScoreAnalysis<>(score, constraintAnalysisList, initialized); - } - - /** - * The domain is based on a single {@link Score} subtype. - * This method is responsible for parsing the score string into that subtype. - * - * @param scoreString never null - * @return never null - */ - protected abstract Score_ parseScore(String scoreString); - - /** - * Each {@link Constraint} in the {@link ConstraintProvider} is justified - * with a custom implementation {@link ConstraintJustification}. - * This method is responsible for telling Jackson which type to serialize the justification into. - * This type must have a deserializer registered. - * - * @param constraintRef never null - * @return null if fallback {@link #parseConstraintJustification(ConstraintRef, String, Score)} should be used instead. - * @param Domain-specific custom implementation, typically constraint-specific. - */ - protected Class - getConstraintJustificationClass(ConstraintRef constraintRef) { - return null; - } - - /** - * Each {@link Constraint} in the {@link ConstraintProvider} is justified - * with a custom implementation {@link ConstraintJustification}. - * This method is responsible for parsing the justification string into that subtype. - * It is a fallback for when using a deserializer for {@link #getConstraintJustificationClass(ConstraintRef)} - * isn't possible - * - * @param constraintRef never null - * @param constraintJustificationString never null - * @param score never null - * @return never null - * @param Domain-specific custom implementation, typically constraint-specific. - */ - protected ConstraintJustification_ - parseConstraintJustification(ConstraintRef constraintRef, String constraintJustificationString, Score_ score) { - throw new UnsupportedOperationException(); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java deleted file mode 100644 index 575c8357e65..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java +++ /dev/null @@ -1,54 +0,0 @@ -package ai.timefold.solver.jackson.api.score.analysis; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; - -public final class ScoreAnalysisJacksonSerializer> extends ValueSerializer> { - @Override - public void serialize(ScoreAnalysis value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException { - gen.writeStartObject(); - gen.writeStringProperty("score", value.score().toString()); - gen.writeBooleanProperty("initialized", value.isSolutionInitialized()); - - List> result = new ArrayList<>(); - value.constraintMap().forEach((constraintRef, constraintAnalysis) -> { - Map constraintAnalysisMap = new LinkedHashMap<>(); - constraintAnalysisMap.put("name", constraintRef.constraintName()); - constraintAnalysisMap.put("weight", constraintAnalysis.weight().toString()); - constraintAnalysisMap.put("score", constraintAnalysis.score().toString()); - if (constraintAnalysis.matches() != null) { - List> matchAnalysis = new ArrayList<>(constraintAnalysis.matches().size()); - constraintAnalysis.matches().forEach(match -> { - Map matchMap = new LinkedHashMap<>(); - matchMap.put("score", match.score().toString()); - if (match.justification() instanceof DefaultConstraintJustification justification) { - matchMap.put("justification", justification.getFacts()); - } else { - matchMap.put("justification", match.justification()); - } - matchAnalysis.add(matchMap); - }); - constraintAnalysisMap.put("matches", matchAnalysis); - } - if (constraintAnalysis.matchCount() != -1) { - constraintAnalysisMap.put("matchCount", constraintAnalysis.matchCount()); - } - result.add(constraintAnalysisMap); - }); - gen.writePOJOProperty("constraints", result); - gen.writeEndObject(); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonDeserializer.java index 147f67bb627..636fc17fb8f 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonDeserializer.java @@ -1,6 +1,6 @@ package ai.timefold.solver.jackson.api.score.constraint; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonSerializer.java index 9800bbe8e6f..2cbe108837f 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/constraint/ConstraintRefJacksonSerializer.java @@ -1,6 +1,6 @@ package ai.timefold.solver.jackson.api.score.constraint; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonGenerator; diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/solver/AbstractRecommendedAssignmentJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/solver/AbstractRecommendedAssignmentJacksonDeserializer.java deleted file mode 100644 index d0a500c2aaa..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/solver/AbstractRecommendedAssignmentJacksonDeserializer.java +++ /dev/null @@ -1,61 +0,0 @@ -package ai.timefold.solver.jackson.api.solver; - -import java.util.concurrent.atomic.AtomicLong; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; -import ai.timefold.solver.core.impl.solver.DefaultRecommendedAssignment; -import ai.timefold.solver.jackson.api.score.analysis.AbstractScoreAnalysisJacksonDeserializer; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonParser; -import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.ValueDeserializer; - -/** - * Extend this to implement {@link RecommendedAssignment} deserialization specific for your domain. - * Make sure to also: - * - *
    - *
  • extend {@link AbstractScoreAnalysisJacksonDeserializer},
  • - *
  • provide deserializer for {@link AbstractRecommendedAssignmentJacksonDeserializer#getPropositionClass()}
  • - *
  • and finally add all of these to the Jackson {@link ObjectMapper}.
  • - *
- * - * @param - */ -public abstract class AbstractRecommendedAssignmentJacksonDeserializer> - extends ValueDeserializer> { - - /** - * {@link DefaultRecommendedAssignment} requires ID for purposes of ordering, - * to break ties if two instances have the same score. - * This ID has no other effect on the instances. - * - *

- * This counter is used to generate an ever-increasing ID. - */ - private final AtomicLong ID_COUNTER = new AtomicLong(0L); - - @Override - public final RecommendedAssignment deserialize(JsonParser p, DeserializationContext ctxt) - throws JacksonException { - JsonNode node = p.readValueAsTree(); - Proposition_ proposition = ctxt.readTreeAsValue(node.get("proposition"), getPropositionClass()); - ScoreAnalysis diff = ctxt.readTreeAsValue(node.get("scoreDiff"), ScoreAnalysis.class); - return new DefaultRecommendedAssignment<>(ID_COUNTER.getAndIncrement(), proposition, diff); - } - - /** - * Each {@link RecommendedAssignment} has a proposition, which is a custom object returned by the user. - * It is therefore the user and only the user who can deserialize the proposition. - * This type must have a deserializer registered. - * - * @return never null - */ - protected abstract Class getPropositionClass(); - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/solver/RecommendedAssignmentJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/solver/RecommendedAssignmentJacksonSerializer.java deleted file mode 100644 index 93f17186b78..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/solver/RecommendedAssignmentJacksonSerializer.java +++ /dev/null @@ -1,21 +0,0 @@ -package ai.timefold.solver.jackson.api.solver; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; - -public final class RecommendedAssignmentJacksonSerializer> - extends ValueSerializer> { - @Override - public void serialize(RecommendedAssignment value, JsonGenerator gen, - SerializationContext serializerProvider) throws JacksonException { - gen.writeStartObject(); - gen.writePOJOProperty("proposition", value.proposition()); - gen.writePOJOProperty("scoreDiff", value.scoreAnalysisDiff()); - gen.writeEndObject(); - } -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningEntityDiffJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningEntityDiffJacksonSerializer.java deleted file mode 100644 index 9413b02df56..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningEntityDiffJacksonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; - -public final class PlanningEntityDiffJacksonSerializer - extends ValueSerializer> { - - @Override - public void serialize(PlanningEntityDiff entityDiff, JsonGenerator jsonGenerator, - SerializationContext serializerProvider) throws JacksonException { - jsonGenerator.writePOJO(SerializablePlanningEntityDiff.of(entityDiff)); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningSolutionDiffJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningSolutionDiffJacksonSerializer.java deleted file mode 100644 index 148806278e7..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningSolutionDiffJacksonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; - -public final class PlanningSolutionDiffJacksonSerializer - extends ValueSerializer> { - - @Override - public void serialize(PlanningSolutionDiff solutionDiff, JsonGenerator jsonGenerator, - SerializationContext serializerProvider) throws JacksonException { - jsonGenerator.writePOJO(SerializablePlanningSolutionDiff.of(solutionDiff)); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningVariableDiffJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningVariableDiffJacksonSerializer.java deleted file mode 100644 index 0212bfbc84c..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningVariableDiffJacksonSerializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; - -public final class PlanningVariableDiffJacksonSerializer - extends ValueSerializer> { - - @Override - public void serialize(PlanningVariableDiff variableDiff, JsonGenerator jsonGenerator, - SerializationContext serializerProvider) throws JacksonException { - jsonGenerator.writePOJO(SerializablePlanningVariableDiff.of(variableDiff)); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningEntityDiff.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningEntityDiff.java deleted file mode 100644 index 7e6919f0940..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningEntityDiff.java +++ /dev/null @@ -1,24 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import java.util.Collection; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; - -import com.fasterxml.jackson.annotation.JsonProperty; - -record SerializablePlanningEntityDiff( - @JsonProperty("entity_class") String entityClass, - @JsonProperty("entity") Entity_ entity, - @JsonProperty("variable_diffs") Collection> variableDiffs) { - - public static SerializablePlanningEntityDiff of(PlanningEntityDiff diff) { - return new SerializablePlanningEntityDiff<>( - diff.entity().getClass().getCanonicalName(), - diff.entity(), - diff.variableDiffs().stream() - .map(SerializablePlanningVariableDiff::of) - .collect(Collectors.toList())); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningSolutionDiff.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningSolutionDiff.java deleted file mode 100644 index bd0bdb8075a..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningSolutionDiff.java +++ /dev/null @@ -1,24 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import java.util.Collection; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; - -import com.fasterxml.jackson.annotation.JsonProperty; - -record SerializablePlanningSolutionDiff( - @JsonProperty("removed_entities") Collection removedEntities, - @JsonProperty("added_entities") Collection addedEntities, - @JsonProperty("entity_diffs") Collection> entityDiffs) { - - public static SerializablePlanningSolutionDiff of(PlanningSolutionDiff diff) { - return new SerializablePlanningSolutionDiff( - diff.removedEntities(), - diff.addedEntities(), - diff.entityDiffs().stream() - .map(SerializablePlanningEntityDiff::of) - .collect(Collectors.toList())); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningVariableDiff.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningVariableDiff.java deleted file mode 100644 index c4bab50f0c2..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/SerializablePlanningVariableDiff.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; - -import com.fasterxml.jackson.annotation.JsonProperty; - -record SerializablePlanningVariableDiff( - @JsonProperty("name") String variableName, - @JsonProperty("old_value") Value_ oldValue, - @JsonProperty("new_value") Value_ newValue) { - - public static SerializablePlanningVariableDiff of(PlanningVariableDiff diff) { - return new SerializablePlanningVariableDiff<>( - diff.variableMetaModel().name(), - diff.oldValue(), - diff.newValue()); - } - -} diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/package-info.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/package-info.java deleted file mode 100644 index cfa1f7bfbc3..00000000000 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Includes support for deserialization of - * {@link ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff}. - * The serialization happens automatically, - * if the user has registered {@link ai.timefold.solver.jackson.api.TimefoldJacksonModule} - * with their {@link tools.jackson.databind.ObjectMapper}. - * - *

- * Deserialization is not implemented, on account of losing the information about the type of the solution, - * its entities and values. - */ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java index fca84da62bb..8b04136a695 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java @@ -2,28 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.ServiceLoader; -import java.util.stream.Collectors; import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.score.BendableScore; import ai.timefold.solver.core.api.score.HardSoftScore; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; -import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; -import ai.timefold.solver.core.impl.solver.DefaultRecommendedAssignment; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.jackson.api.domain.solution.AbstractConstraintWeightOverridesDeserializer; -import ai.timefold.solver.jackson.api.score.analysis.AbstractScoreAnalysisJacksonDeserializer; -import ai.timefold.solver.jackson.api.solver.AbstractRecommendedAssignmentJacksonDeserializer; import org.junit.jupiter.api.Test; @@ -35,7 +21,6 @@ import tools.jackson.databind.MapperFeature; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.module.SimpleModule; -import tools.jackson.databind.type.TypeFactory; class TimefoldJacksonModuleTest extends AbstractJacksonRoundTripTest { @@ -68,153 +53,6 @@ void polymorphicScore() { .isEqualTo(BendableScore.of(new long[] { -1, -20 }, new long[] { -300, -4000, -50000 })); } - @Test - void scoreAnalysisWithoutMatches() throws JacksonException { - var objectMapper = JsonMapper.builder() - .addModule(TimefoldJacksonModule.createModule()) - .addModule(new CustomJacksonModule()) - .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT)) - .build(); - - var constraintRef1 = ConstraintRef.of("constraint1"); - var constraintRef2 = ConstraintRef.of("constraint2"); - var constraintAnalysis1 = - new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(2), null); - var constraintAnalysis2 = - new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofHard(1), HardSoftScore.ofHard(1), null, 2); - var originalScoreAnalysis = new ScoreAnalysis<>(HardSoftScore.of(1, 2), - Map.of(constraintRef1, constraintAnalysis1, - constraintRef2, constraintAnalysis2)); - - // Hardest constraints first, package name second. - var serialized = objectMapper.writeValueAsString(originalScoreAnalysis); - assertThat(serialized) - .isEqualToIgnoringWhitespace(""" - { - "score" : "1hard/2soft", - "initialized" : true, - "constraints" : [ { - "name" : "constraint2", - "weight" : "1hard/0soft", - "score" : "1hard/0soft", - "matchCount" : 2 - }, { - "name" : "constraint1", - "weight" : "0hard/1soft", - "score" : "0hard/2soft" - } ] - }"""); - - ScoreAnalysis deserialized = objectMapper.readValue(serialized, ScoreAnalysis.class); - assertThat(deserialized).isEqualTo(originalScoreAnalysis); - } - - @Test - void scoreAnalysisWithMatches() throws JacksonException { - var objectMapper = JsonMapper.builder() - .addModule(TimefoldJacksonModule.createModule()) - .addModule(new CustomJacksonModule()) - .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT)) - .build(); - - var originalScoreAnalysis = getScoreAnalysis(); - var serialized = objectMapper.writeValueAsString(originalScoreAnalysis); - assertThat(serialized) - .isEqualToIgnoringWhitespace(getSerializedScoreAnalysis()); - - ScoreAnalysis deserialized = objectMapper.readValue(serialized, ScoreAnalysis.class); - assertThat(deserialized).isEqualTo(originalScoreAnalysis); - } - - private static ScoreAnalysis getScoreAnalysis() { - var constraintRef1 = ConstraintRef.of("constraint1"); - var constraintRef2 = ConstraintRef.of("constraint2"); - var matchAnalysis1 = new MatchAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), - DefaultConstraintJustification.of(HardSoftScore.ofHard(1), "A", "B")); - var matchAnalysis2 = new MatchAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), - DefaultConstraintJustification.of(HardSoftScore.ofHard(1), "B", "C", "D")); - var matchAnalysis3 = new MatchAnalysis<>(constraintRef2, HardSoftScore.ofSoft(1), - DefaultConstraintJustification.of(HardSoftScore.ofSoft(1), "D")); - var matchAnalysis4 = new MatchAnalysis<>(constraintRef2, HardSoftScore.ofSoft(3), - DefaultConstraintJustification.of(HardSoftScore.ofSoft(3), "A", "C")); - var constraintAnalysis1 = - new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), HardSoftScore.ofHard(2), - List.of(matchAnalysis1, matchAnalysis2)); - var constraintAnalysis2 = - new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(4), - List.of(matchAnalysis3, matchAnalysis4)); - return new ScoreAnalysis<>(HardSoftScore.of(2, 4), - Map.of(constraintRef1, constraintAnalysis1, - constraintRef2, constraintAnalysis2)); - } - - private static String getSerializedScoreAnalysis() { - return """ - { - "score" : "2hard/4soft", - "initialized" : true, - "constraints" : [ { - "name" : "constraint1", - "weight" : "1hard/0soft", - "score" : "2hard/0soft", - "matches" : [ { - "score" : "1hard/0soft", - "justification" : [ "A", "B" ] - }, { - "score" : "1hard/0soft", - "justification" : [ "B", "C", "D" ] - } - ], - "matchCount" : 2 - }, { - "name" : "constraint2", - "weight" : "0hard/1soft", - "score" : "0hard/4soft", - "matches" : [ { - "score" : "0hard/1soft", - "justification" : [ "D" ] - }, { - "score" : "0hard/3soft", - "justification" : [ "A", "C" ] - } ], - "matchCount" : 2 - } ] - }"""; - } - - @Test - void recommendedAssignment() throws JacksonException { - var objectMapper = JsonMapper.builder() - .addModule(TimefoldJacksonModule.createModule()) - .addModule(new CustomJacksonModule()) - .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT)) - .build(); - - var proposition = new Pair<>("A", "1"); - var originalScoreAnalysis = getScoreAnalysis(); - var originalRecommendedAssignment = new DefaultRecommendedAssignment<>(0, proposition, originalScoreAnalysis); - var fitList = List.of(originalRecommendedAssignment); - - var serialized = objectMapper.writeValueAsString(fitList); - assertThat(serialized) - .isEqualToIgnoringWhitespace(""" - [ { - "proposition" : { - "key" : "A", - "value" : "1" - }, - "scoreDiff" : %s - } ]""".formatted(getSerializedScoreAnalysis())); - - List, HardSoftScore>> deserialized = - objectMapper.readValue(serialized, - TypeFactory.createDefaultInstance().constructCollectionType(List.class, RecommendedAssignment.class)); - assertThat(deserialized) - .hasSize(1) - .first() - .isEqualTo(originalRecommendedAssignment); - } - @Test void constraintWeightOverrides() throws JacksonException { var objectMapper = JsonMapper.builder() @@ -250,8 +88,6 @@ public static final class CustomJacksonModule extends SimpleModule { public CustomJacksonModule() { super("Timefold Custom"); - addDeserializer(ScoreAnalysis.class, new CustomScoreAnalysisJacksonDeserializer()); - addDeserializer(RecommendedAssignment.class, new CustomRecommendedAssignmentJacksonDeserializer()); addDeserializer(ConstraintWeightOverrides.class, new CustomConstraintWeightOverridesDeserializer()); } @@ -267,38 +103,6 @@ protected HardSoftScore parseScore(String scoreString) { } - public static final class CustomScoreAnalysisJacksonDeserializer - extends AbstractScoreAnalysisJacksonDeserializer { - - @Override - protected HardSoftScore parseScore(String scoreString) { - return HardSoftScore.parseScore(scoreString); - } - - @Override - protected ConstraintJustification_ - parseConstraintJustification(ConstraintRef constraintRef, String constraintJustificationString, - HardSoftScore score) { - List justificationList = Arrays.stream(constraintJustificationString.split(",")) - .map(s -> s.replace("[", "") - .replace("]", "") - .replace("\"", "") - .strip()) - .collect(Collectors.toList()); - return (ConstraintJustification_) DefaultConstraintJustification.of(score, justificationList); - } - - } - - public static final class CustomRecommendedAssignmentJacksonDeserializer - extends AbstractRecommendedAssignmentJacksonDeserializer, HardSoftScore> { - - @Override - protected Class> getPropositionClass() { - return (Class) Pair.class; - } - } - public static class TestTimefoldJacksonModuleWrapper { private BendableScore bendableScore; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonRoundTripTest.java index 1a70d35287f..0dc9431ac6a 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableBigDecimalScoreJacksonRoundTripTest.java @@ -3,8 +3,6 @@ import java.math.BigDecimal; import ai.timefold.solver.core.api.score.BendableBigDecimalScore; -import ai.timefold.solver.jackson.api.score.buildin.BendableBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.BendableBigDecimalScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonRoundTripTest.java index 46528f9848d..5c1c4041236 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/BendableScoreJacksonRoundTripTest.java @@ -1,8 +1,6 @@ package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.BendableScore; -import ai.timefold.solver.jackson.api.score.buildin.BendableScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.BendableScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonRoundTripTest.java index 32498ac28ea..7e5f0a3c631 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftBigDecimalScoreJacksonRoundTripTest.java @@ -3,8 +3,6 @@ import java.math.BigDecimal; import ai.timefold.solver.core.api.score.HardMediumSoftBigDecimalScore; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftBigDecimalScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonRoundTripTest.java index 07f8f01fea4..c7207c5dc24 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardMediumSoftScoreJacksonRoundTripTest.java @@ -1,8 +1,6 @@ package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardMediumSoftScore; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonRoundTripTest.java index 4ae98fe6e41..f9a5c75ef39 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftBigDecimalScoreJacksonRoundTripTest.java @@ -3,8 +3,6 @@ import java.math.BigDecimal; import ai.timefold.solver.core.api.score.HardSoftBigDecimalScore; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftBigDecimalScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonRoundTripTest.java index aa6e712f4cf..1da9c8af98e 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/HardSoftScoreJacksonRoundTripTest.java @@ -1,8 +1,6 @@ package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.HardSoftScore; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonRoundTripTest.java index e86e8ae36fe..24613512ae5 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleBigDecimalScoreJacksonRoundTripTest.java @@ -3,8 +3,6 @@ import java.math.BigDecimal; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; -import ai.timefold.solver.jackson.api.score.buildin.SimpleBigDecimalScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleBigDecimalScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonRoundTripTest.java index d307d26df3b..433d32faaa6 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/SimpleScoreJacksonRoundTripTest.java @@ -1,8 +1,6 @@ package ai.timefold.solver.jackson.api.score; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonSerializer; import org.junit.jupiter.api.Test; diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningSolutionDiffTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningSolutionDiffTest.java deleted file mode 100644 index 1d5ee512db1..00000000000 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/preview/api/domain/solution/diff/PlanningSolutionDiffTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package ai.timefold.solver.jackson.preview.api.domain.solution.diff; - -import static org.assertj.core.api.Assertions.assertThat; - -import ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeSolution; -import ai.timefold.solver.jackson.api.TimefoldJacksonModule; - -import org.junit.jupiter.api.Test; - -import tools.jackson.core.JacksonException; -import tools.jackson.databind.MapperFeature; -import tools.jackson.databind.json.JsonMapper; - -class PlanningSolutionDiffTest { - - @Test - void serialize() throws JacksonException { - var oldSolution = TestdataEqualsByCodeSolution.generateSolution("A", 2, 2); - oldSolution.getEntityList().get(0).setValue(oldSolution.getValueList().get(1)); - var newSolution = TestdataEqualsByCodeSolution.generateSolution("B", 3, 3); - var solutionDescriptor = TestdataEqualsByCodeSolution.buildSolutionDescriptor(); - var diff = solutionDescriptor.diff(oldSolution, newSolution); - - var objectMapper = JsonMapper.builder() - .addModule(TimefoldJacksonModule.createModule()) - .disable(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST) - .build(); - - var serialized = objectMapper.writeValueAsString(diff); - assertThat(serialized) - .isEqualToIgnoringWhitespace(""" - { - "added_entities": [ - { - "code": "Generated Entity 2", - "value": { - "code": "Generated Value 2" - } - } - ], - "entity_diffs": [ - { - "entity": { - "code": "Generated Entity 0", - "value": { - "code": "Generated Value 1" - } - }, - "entity_class": "ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeEntity", - "variable_diffs": [ - { - "name": "value", - "new_value": { - "code": "Generated Value 0" - }, - "old_value": { - "code": "Generated Value 1" - } - } - ] - } - ], - "removed_entities": [] - }"""); - - } - -} diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/testdomain/JacksonTestdataSolution.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/testdomain/JacksonTestdataSolution.java index 9204a3e4232..970514d3f7f 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/testdomain/JacksonTestdataSolution.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/testdomain/JacksonTestdataSolution.java @@ -9,8 +9,8 @@ import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonDeserializer; -import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonSerializer; +import ai.timefold.solver.jackson.api.score.SimpleScoreJacksonDeserializer; +import ai.timefold.solver.jackson.api.score.SimpleScoreJacksonSerializer; import tools.jackson.databind.annotation.JsonDeserialize; import tools.jackson.databind.annotation.JsonSerialize; diff --git a/quarkus-integration/quarkus-jackson/deployment/pom.xml b/quarkus-integration/quarkus-jackson/deployment/pom.xml index 35a86130262..6dd8a58c4bc 100644 --- a/quarkus-integration/quarkus-jackson/deployment/pom.xml +++ b/quarkus-integration/quarkus-jackson/deployment/pom.xml @@ -15,6 +15,10 @@ https://solver.timefold.ai + + ai.timefold.solver + timefold-solver-quarkus-jackson + io.quarkus quarkus-core-deployment @@ -28,10 +32,6 @@ io.quarkus quarkus-jackson-spi - - ai.timefold.solver - timefold-solver-quarkus-jackson - diff --git a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/TimefoldTestResource.java b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/TimefoldTestResource.java index f1ba9b00233..cc574f4e3ca 100644 --- a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/TimefoldTestResource.java +++ b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/TimefoldTestResource.java @@ -8,22 +8,22 @@ import ai.timefold.solver.core.api.solver.SolverJob; import ai.timefold.solver.core.api.solver.SolverManager; -import ai.timefold.solver.quarkus.jackson.it.domain.ITestdataPlanningSolution; +import ai.timefold.solver.quarkus.jackson.it.testdata.TestdataJacksonPlanningSolution; @Path("/timefold/test") public class TimefoldTestResource { - private final SolverManager solverManager; + private final SolverManager solverManager; @Inject - public TimefoldTestResource(SolverManager solverManager) { + public TimefoldTestResource(SolverManager solverManager) { this.solverManager = solverManager; } @POST @Path("/solver-factory") - public ITestdataPlanningSolution solveWithSolverFactory(ITestdataPlanningSolution problem) { - SolverJob solverJob = solverManager.solve(1L, problem); + public TestdataJacksonPlanningSolution solveWithSolverFactory(TestdataJacksonPlanningSolution problem) { + SolverJob solverJob = solverManager.solve(1L, problem); try { return solverJob.getFinalBestSolution(); } catch (InterruptedException e) { diff --git a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/solver/ITestdataPlanningConstraintProvider.java b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningConstraintProvider.java similarity index 64% rename from quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/solver/ITestdataPlanningConstraintProvider.java rename to quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningConstraintProvider.java index 5a7a61657b8..5a0d0d15bab 100644 --- a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/solver/ITestdataPlanningConstraintProvider.java +++ b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningConstraintProvider.java @@ -1,21 +1,20 @@ -package ai.timefold.solver.quarkus.jackson.it.solver; +package ai.timefold.solver.quarkus.jackson.it.testdata; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.score.stream.Joiners; -import ai.timefold.solver.quarkus.jackson.it.domain.ITestdataPlanningEntity; import org.jspecify.annotations.NonNull; -public class ITestdataPlanningConstraintProvider implements ConstraintProvider { +public class TestdataJacksonPlanningConstraintProvider implements ConstraintProvider { @Override public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory factory) { return new Constraint[] { - factory.forEach(ITestdataPlanningEntity.class) - .join(ITestdataPlanningEntity.class, Joiners.equal(ITestdataPlanningEntity::getValue)) + factory.forEach(TestdataJacksonPlanningEntity.class) + .join(TestdataJacksonPlanningEntity.class, Joiners.equal(TestdataJacksonPlanningEntity::getValue)) .filter((a, b) -> a != b) .penalize(SimpleScore.ONE) .asConstraint("Don't assign 2 entities the same value.") diff --git a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/domain/ITestdataPlanningEntity.java b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningEntity.java similarity index 84% rename from quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/domain/ITestdataPlanningEntity.java rename to quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningEntity.java index 532d18203ad..5df48bf23b9 100644 --- a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/domain/ITestdataPlanningEntity.java +++ b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningEntity.java @@ -1,10 +1,10 @@ -package ai.timefold.solver.quarkus.jackson.it.domain; +package ai.timefold.solver.quarkus.jackson.it.testdata; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; @PlanningEntity -public class ITestdataPlanningEntity { +public class TestdataJacksonPlanningEntity { @PlanningVariable(valueRangeProviderRefs = "valueRange") private String value; diff --git a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/domain/ITestdataPlanningSolution.java b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningSolution.java similarity index 78% rename from quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/domain/ITestdataPlanningSolution.java rename to quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningSolution.java index eed7b6ae715..f383324242a 100644 --- a/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/domain/ITestdataPlanningSolution.java +++ b/quarkus-integration/quarkus-jackson/integration-test/src/main/java/ai/timefold/solver/quarkus/jackson/it/testdata/TestdataJacksonPlanningSolution.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.quarkus.jackson.it.domain; +package ai.timefold.solver.quarkus.jackson.it.testdata; import java.util.List; @@ -9,12 +9,12 @@ import ai.timefold.solver.core.api.score.SimpleScore; @PlanningSolution -public class ITestdataPlanningSolution { +public class TestdataJacksonPlanningSolution { @ValueRangeProvider(id = "valueRange") private List valueList; @PlanningEntityCollectionProperty - private List entityList; + private List entityList; @PlanningScore private SimpleScore score; @@ -31,11 +31,11 @@ public void setValueList(List valueList) { this.valueList = valueList; } - public List getEntityList() { + public List getEntityList() { return entityList; } - public void setEntityList(List entityList) { + public void setEntityList(List entityList) { this.entityList = entityList; } diff --git a/quarkus-integration/quarkus-jackson/runtime/pom.xml b/quarkus-integration/quarkus-jackson/runtime/pom.xml index b26078f9fb5..042ae099bfb 100644 --- a/quarkus-integration/quarkus-jackson/runtime/pom.xml +++ b/quarkus-integration/quarkus-jackson/runtime/pom.xml @@ -83,11 +83,6 @@ extension-descriptor compile - - - ai.timefold.solver.timefold-quarkus-jackson - - diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModule.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModule.java index 3e3c6c6a58e..9c3d3524c07 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModule.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModule.java @@ -10,21 +10,12 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.score.stream.common.Break; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.common.Sequence; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; import ai.timefold.solver.core.impl.domain.solution.DefaultConstraintWeightOverrides; -import ai.timefold.solver.core.impl.solver.DefaultRecommendedAssignment; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; -import ai.timefold.solver.quarkus.jackson.diff.PlanningEntityDiffJacksonSerializer; -import ai.timefold.solver.quarkus.jackson.diff.PlanningSolutionDiffJacksonSerializer; -import ai.timefold.solver.quarkus.jackson.diff.PlanningVariableDiffJacksonSerializer; import ai.timefold.solver.quarkus.jackson.domain.solution.ConstraintWeightOverridesSerializer; import ai.timefold.solver.quarkus.jackson.score.BendableBigDecimalScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.BendableBigDecimalScoreJacksonSerializer; @@ -44,7 +35,6 @@ import ai.timefold.solver.quarkus.jackson.score.SimpleBigDecimalScoreJacksonSerializer; import ai.timefold.solver.quarkus.jackson.score.SimpleScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.SimpleScoreJacksonSerializer; -import ai.timefold.solver.quarkus.jackson.score.analysis.ScoreAnalysisJacksonSerializer; import ai.timefold.solver.quarkus.jackson.score.constraint.ConstraintRefJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.constraint.ConstraintRefJacksonSerializer; import ai.timefold.solver.quarkus.jackson.score.stream.common.BreakJacksonDeserializer; @@ -56,9 +46,7 @@ import ai.timefold.solver.quarkus.jackson.score.stream.common.SequenceJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.stream.common.SequenceJacksonSerializer; import ai.timefold.solver.quarkus.jackson.solution.JacksonSolutionFileIO; -import ai.timefold.solver.quarkus.jackson.solver.RecommendedAssignmentJacksonSerializer; -import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -80,9 +68,13 @@ public static Module createModule() { } - @SuppressWarnings({ "rawtypes", "unchecked" }) public TimefoldJacksonModule() { - super("Timefold"); + this("Timefold"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TimefoldJacksonModule(String name) { + super(name); // For non-subtype Score fields/properties, we also need to record the score type addSerializer(Score.class, new PolymorphicScoreJacksonSerializer()); addDeserializer(Score.class, new PolymorphicScoreJacksonDeserializer()); @@ -104,15 +96,9 @@ public TimefoldJacksonModule() { addSerializer(BendableBigDecimalScore.class, new BendableBigDecimalScoreJacksonSerializer()); addDeserializer(BendableBigDecimalScore.class, new BendableBigDecimalScoreJacksonDeserializer()); - // Score analysis + // Constraint weights addSerializer(ConstraintRef.class, new ConstraintRefJacksonSerializer()); addDeserializer(ConstraintRef.class, new ConstraintRefJacksonDeserializer()); - addSerializer(ScoreAnalysis.class, new ScoreAnalysisJacksonSerializer()); - var serializer = (JsonSerializer) new RecommendedAssignmentJacksonSerializer<>(); - addSerializer(RecommendedAssignment.class, serializer); - addSerializer(DefaultRecommendedAssignment.class, serializer); - - // Constraint weights addSerializer(ConstraintWeightOverrides.class, new ConstraintWeightOverridesSerializer()); addSerializer(DefaultConstraintWeightOverrides.class, new ConstraintWeightOverridesSerializer()); @@ -125,11 +111,6 @@ public TimefoldJacksonModule() { addDeserializer(SequenceChain.class, new SequenceChainJacksonDeserializer<>()); addSerializer(LoadBalance.class, new LoadBalanceJacksonSerializer()); addDeserializer(LoadBalance.class, new LoadBalanceJacksonDeserializer<>()); - - // Solution diff - addSerializer(PlanningSolutionDiff.class, new PlanningSolutionDiffJacksonSerializer()); - addSerializer(PlanningEntityDiff.class, new PlanningEntityDiffJacksonSerializer()); - addSerializer(PlanningVariableDiff.class, new PlanningVariableDiffJacksonSerializer()); } } diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningEntityDiffJacksonSerializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningEntityDiffJacksonSerializer.java deleted file mode 100644 index c3b85ab577a..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningEntityDiffJacksonSerializer.java +++ /dev/null @@ -1,20 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import java.io.IOException; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -public final class PlanningEntityDiffJacksonSerializer - extends JsonSerializer> { - - @Override - public void serialize(PlanningEntityDiff entityDiff, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeObject(SerializablePlanningEntityDiff.of(entityDiff)); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningSolutionDiffJacksonSerializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningSolutionDiffJacksonSerializer.java deleted file mode 100644 index ad87549b421..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningSolutionDiffJacksonSerializer.java +++ /dev/null @@ -1,20 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import java.io.IOException; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -public final class PlanningSolutionDiffJacksonSerializer - extends JsonSerializer> { - - @Override - public void serialize(PlanningSolutionDiff solutionDiff, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeObject(SerializablePlanningSolutionDiff.of(solutionDiff)); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningVariableDiffJacksonSerializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningVariableDiffJacksonSerializer.java deleted file mode 100644 index 42ff96c0960..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/PlanningVariableDiffJacksonSerializer.java +++ /dev/null @@ -1,20 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import java.io.IOException; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -public final class PlanningVariableDiffJacksonSerializer - extends JsonSerializer> { - - @Override - public void serialize(PlanningVariableDiff variableDiff, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeObject(SerializablePlanningVariableDiff.of(variableDiff)); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningEntityDiff.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningEntityDiff.java deleted file mode 100644 index 81d1b2bbec2..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningEntityDiff.java +++ /dev/null @@ -1,24 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import java.util.Collection; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningEntityDiff; - -import com.fasterxml.jackson.annotation.JsonProperty; - -record SerializablePlanningEntityDiff( - @JsonProperty("entity_class") String entityClass, - @JsonProperty("entity") Entity_ entity, - @JsonProperty("variable_diffs") Collection> variableDiffs) { - - public static SerializablePlanningEntityDiff of(PlanningEntityDiff diff) { - return new SerializablePlanningEntityDiff<>( - diff.entity().getClass().getCanonicalName(), - diff.entity(), - diff.variableDiffs().stream() - .map(SerializablePlanningVariableDiff::of) - .collect(Collectors.toList())); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningSolutionDiff.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningSolutionDiff.java deleted file mode 100644 index 8665b097c0f..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningSolutionDiff.java +++ /dev/null @@ -1,24 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import java.util.Collection; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff; - -import com.fasterxml.jackson.annotation.JsonProperty; - -record SerializablePlanningSolutionDiff( - @JsonProperty("removed_entities") Collection removedEntities, - @JsonProperty("added_entities") Collection addedEntities, - @JsonProperty("entity_diffs") Collection> entityDiffs) { - - public static SerializablePlanningSolutionDiff of(PlanningSolutionDiff diff) { - return new SerializablePlanningSolutionDiff( - diff.removedEntities(), - diff.addedEntities(), - diff.entityDiffs().stream() - .map(SerializablePlanningEntityDiff::of) - .collect(Collectors.toList())); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningVariableDiff.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningVariableDiff.java deleted file mode 100644 index 4d229aba83b..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/SerializablePlanningVariableDiff.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningVariableDiff; - -import com.fasterxml.jackson.annotation.JsonProperty; - -record SerializablePlanningVariableDiff( - @JsonProperty("name") String variableName, - @JsonProperty("old_value") Value_ oldValue, - @JsonProperty("new_value") Value_ newValue) { - - public static SerializablePlanningVariableDiff of(PlanningVariableDiff diff) { - return new SerializablePlanningVariableDiff<>( - diff.variableMetaModel().name(), - diff.oldValue(), - diff.newValue()); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/package-info.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/package-info.java deleted file mode 100644 index ecec4b46e18..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/diff/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Includes support for deserialization of - * {@link ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff}. - * The serialization happens automatically, - * if the user has registered {@link ai.timefold.solver.quarkus.jackson.TimefoldJacksonModule} - * with their {@link com.fasterxml.jackson.databind.ObjectMapper}. - * - *

- * Deserialization is not implemented, on account of losing the information about the type of the solution, - * its entities and values. - */ -package ai.timefold.solver.quarkus.jackson.diff; diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java deleted file mode 100644 index a45bec6bce2..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java +++ /dev/null @@ -1,115 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.score.analysis; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; -import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.Constraint; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.ConstraintProvider; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Extend this to implement {@link ScoreAnalysis} deserialization specific for your domain. - * - * @param - */ -public abstract class AbstractScoreAnalysisJacksonDeserializer> - extends JsonDeserializer> { - - @Override - public final ScoreAnalysis deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonNode node = p.readValueAsTree(); - var score = parseScore(node.get("score").asText()); - var initialized = node.get("initialized").asBoolean(); - var constraintAnalysisList = new HashMap>(); - for (var constraintNode : node.get("constraints")) { - var constraintName = constraintNode.get("name").asText(); - var constraintRef = ConstraintRef.of(constraintName); - var constraintWeight = parseScore(constraintNode.get("weight").asText()); - var constraintScore = parseScore(constraintNode.get("score").asText()); - var matchScoreList = new ArrayList>(); - var matchesNode = constraintNode.get("matches"); - var matchCountNode = constraintNode.get("matchCount"); - if (matchesNode == null) { - constraintAnalysisList.put(constraintRef, - new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, null, - matchCountNode == null ? -1 : matchCountNode.asInt(-1))); - } else { - for (var matchNode : constraintNode.get("matches")) { - var matchScore = parseScore(matchNode.get("score").asText()); - var justificationNode = matchNode.get("justification"); - if (justificationNode == null) { - // Not allowed; if matches are present, they must have justifications. - throw new IllegalStateException("The match justification of constraint (%s)'s match is missing." - .formatted(constraintRef)); - } - var justificationString = justificationNode.toString(); - if (getConstraintJustificationClass(constraintRef) == null) { // String-based fallback. - var parsedJustification = parseConstraintJustification(constraintRef, justificationString, matchScore); - matchScoreList.add(new MatchAnalysis<>(constraintRef, matchScore, parsedJustification)); - } else { // Deserializer-based method. - var parsedJustification = - ctxt.readTreeAsValue(justificationNode, getConstraintJustificationClass(constraintRef)); - matchScoreList.add(new MatchAnalysis<>(constraintRef, matchScore, parsedJustification)); - } - } - constraintAnalysisList.put(constraintRef, - new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, matchScoreList)); - } - } - return new ScoreAnalysis<>(score, constraintAnalysisList, initialized); - } - - /** - * The domain is based on a single {@link Score} subtype. - * This method is responsible for parsing the score string into that subtype. - * - * @param scoreString never null - * @return never null - */ - protected abstract Score_ parseScore(String scoreString); - - /** - * Each {@link Constraint} in the {@link ConstraintProvider} is justified - * with a custom implementation {@link ConstraintJustification}. - * This method is responsible for telling Jackson which type to serialize the justification into. - * This type must have a deserializer registered. - * - * @param constraintRef never null - * @return null if fallback {@link #parseConstraintJustification(ConstraintRef, String, Score)} should be used instead. - * @param Domain-specific custom implementation, typically constraint-specific. - */ - protected Class - getConstraintJustificationClass(ConstraintRef constraintRef) { - return null; - } - - /** - * Each {@link Constraint} in the {@link ConstraintProvider} is justified - * with a custom implementation {@link ConstraintJustification}. - * This method is responsible for parsing the justification string into that subtype. - * It is a fallback for when using a deserializer for {@link #getConstraintJustificationClass(ConstraintRef)} - * isn't possible - * - * @param constraintRef never null - * @param constraintJustificationString never null - * @param score never null - * @return never null - * @param Domain-specific custom implementation, typically constraint-specific. - */ - protected ConstraintJustification_ - parseConstraintJustification(ConstraintRef constraintRef, String constraintJustificationString, Score_ score) { - throw new UnsupportedOperationException(); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/analysis/ScoreAnalysisJacksonSerializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/analysis/ScoreAnalysisJacksonSerializer.java deleted file mode 100644 index 36ed9806197..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/analysis/ScoreAnalysisJacksonSerializer.java +++ /dev/null @@ -1,53 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.score.analysis; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -public final class ScoreAnalysisJacksonSerializer> extends JsonSerializer> { - @Override - public void serialize(ScoreAnalysis value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeStartObject(); - gen.writeStringField("score", value.score().toString()); - gen.writeBooleanField("initialized", value.isSolutionInitialized()); - - List> result = new ArrayList<>(); - value.constraintMap().forEach((constraintRef, constraintAnalysis) -> { - Map constraintAnalysisMap = new LinkedHashMap<>(); - constraintAnalysisMap.put("name", constraintRef.constraintName()); - constraintAnalysisMap.put("weight", constraintAnalysis.weight().toString()); - constraintAnalysisMap.put("score", constraintAnalysis.score().toString()); - if (constraintAnalysis.matches() != null) { - List> matchAnalysis = new ArrayList<>(constraintAnalysis.matches().size()); - constraintAnalysis.matches().forEach(match -> { - Map matchMap = new LinkedHashMap<>(); - matchMap.put("score", match.score().toString()); - if (match.justification() instanceof DefaultConstraintJustification justification) { - matchMap.put("justification", justification.getFacts()); - } else { - matchMap.put("justification", match.justification()); - } - matchAnalysis.add(matchMap); - }); - constraintAnalysisMap.put("matches", matchAnalysis); - } - if (constraintAnalysis.matchCount() != -1) { - constraintAnalysisMap.put("matchCount", constraintAnalysis.matchCount()); - } - result.add(constraintAnalysisMap); - }); - gen.writeObjectField("constraints", result); - gen.writeEndObject(); - } - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonDeserializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonDeserializer.java index b04c4052f6c..384b697ce35 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonDeserializer.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonDeserializer.java @@ -2,7 +2,7 @@ import java.io.IOException; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonSerializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonSerializer.java index f1f74e6a85f..2771e8fa527 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonSerializer.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/score/constraint/ConstraintRefJacksonSerializer.java @@ -2,7 +2,7 @@ import java.io.IOException; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/solver/AbstractRecommendedAssignmentJacksonDeserializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/solver/AbstractRecommendedAssignmentJacksonDeserializer.java deleted file mode 100644 index 3b9ff00a9c5..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/solver/AbstractRecommendedAssignmentJacksonDeserializer.java +++ /dev/null @@ -1,61 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.solver; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicLong; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; -import ai.timefold.solver.core.impl.solver.DefaultRecommendedAssignment; -import ai.timefold.solver.quarkus.jackson.score.analysis.AbstractScoreAnalysisJacksonDeserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Extend this to implement {@link RecommendedAssignment} deserialization specific for your domain. - * Make sure to also: - * - *

    - *
  • extend {@link AbstractScoreAnalysisJacksonDeserializer},
  • - *
  • provide deserializer for {@link AbstractRecommendedAssignmentJacksonDeserializer#getPropositionClass()}
  • - *
  • and finally add all of these to the Jackson {@link ObjectMapper}.
  • - *
- * - * @param - */ -public abstract class AbstractRecommendedAssignmentJacksonDeserializer> - extends JsonDeserializer> { - - /** - * {@link DefaultRecommendedAssignment} requires ID for purposes of ordering, - * to break ties if two instances have the same score. - * This ID has no other effect on the instances. - * - *

- * This counter is used to generate an ever-increasing ID. - */ - private final AtomicLong ID_COUNTER = new AtomicLong(0L); - - @Override - public final RecommendedAssignment deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException { - JsonNode node = p.readValueAsTree(); - Proposition_ proposition = ctxt.readTreeAsValue(node.get("proposition"), getPropositionClass()); - ScoreAnalysis diff = ctxt.readTreeAsValue(node.get("scoreDiff"), ScoreAnalysis.class); - return new DefaultRecommendedAssignment<>(ID_COUNTER.getAndIncrement(), proposition, diff); - } - - /** - * Each {@link RecommendedAssignment} has a proposition, which is a custom object returned by the user. - * It is therefore the user and only the user who can deserialize the proposition. - * This type must have a deserializer registered. - * - * @return never null - */ - protected abstract Class getPropositionClass(); - -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/solver/RecommendedAssignmentJacksonSerializer.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/solver/RecommendedAssignmentJacksonSerializer.java deleted file mode 100644 index 582ff4d11f6..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/ai/timefold/solver/quarkus/jackson/solver/RecommendedAssignmentJacksonSerializer.java +++ /dev/null @@ -1,22 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.solver; - -import java.io.IOException; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -public final class RecommendedAssignmentJacksonSerializer> - extends JsonSerializer> { - @Override - public void serialize(RecommendedAssignment value, JsonGenerator gen, - SerializerProvider serializerProvider) throws IOException { - gen.writeStartObject(); - gen.writeObjectField("proposition", value.proposition()); - gen.writeObjectField("scoreDiff", value.scoreAnalysisDiff()); - gen.writeEndObject(); - } -} diff --git a/quarkus-integration/quarkus-jackson/runtime/src/main/java/module-info.java b/quarkus-integration/quarkus-jackson/runtime/src/main/java/module-info.java index 373bacfa6a1..63d52f6ac29 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/main/java/module-info.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/main/java/module-info.java @@ -1,6 +1,7 @@ module ai.timefold.solver.quarkus.jackson { exports ai.timefold.solver.quarkus.jackson; + exports ai.timefold.solver.quarkus.jackson.solution; requires transitive ai.timefold.solver.core; requires com.fasterxml.jackson.databind; diff --git a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModuleTest.java b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModuleTest.java index 7c785aad26e..a79e2caec5c 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModuleTest.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/TimefoldJacksonModuleTest.java @@ -2,27 +2,13 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides; import ai.timefold.solver.core.api.score.BendableScore; import ai.timefold.solver.core.api.score.HardSoftScore; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; -import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; -import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; -import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; -import ai.timefold.solver.core.api.solver.RecommendedAssignment; -import ai.timefold.solver.core.impl.solver.DefaultRecommendedAssignment; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.quarkus.jackson.domain.solution.AbstractConstraintWeightOverridesDeserializer; -import ai.timefold.solver.quarkus.jackson.score.analysis.AbstractScoreAnalysisJacksonDeserializer; -import ai.timefold.solver.quarkus.jackson.solver.AbstractRecommendedAssignmentJacksonDeserializer; import org.junit.jupiter.api.Test; @@ -31,7 +17,6 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.type.TypeFactory; class TimefoldJacksonModuleTest extends AbstractJacksonRoundTripTest { @@ -64,159 +49,6 @@ void polymorphicScore() { .isEqualTo(BendableScore.of(new long[] { -1, -20 }, new long[] { -300, -4000, -50000 })); } - @Test - void scoreAnalysisWithoutMatches() throws JsonProcessingException { - var objectMapper = JsonMapper.builder() - .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .defaultPropertyInclusion( - JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)) - .addModule(TimefoldJacksonModule.createModule()) - .build(); - - var constraintRef1 = ConstraintRef.of("constraint1"); - var constraintRef2 = ConstraintRef.of("constraint2"); - var constraintAnalysis1 = - new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(2), null); - var constraintAnalysis2 = - new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofHard(1), HardSoftScore.ofHard(1), null, 2); - var originalScoreAnalysis = new ScoreAnalysis<>(HardSoftScore.of(1, 2), - Map.of(constraintRef1, constraintAnalysis1, - constraintRef2, constraintAnalysis2)); - - // Hardest constraints first, package name second. - var serialized = objectMapper.writeValueAsString(originalScoreAnalysis); - assertThat(serialized) - .isEqualToIgnoringWhitespace(""" - { - "score" : "1hard/2soft", - "initialized" : true, - "constraints" : [ { - "name" : "constraint2", - "weight" : "1hard/0soft", - "score" : "1hard/0soft", - "matchCount" : 2 - }, { - "name" : "constraint1", - "weight" : "0hard/1soft", - "score" : "0hard/2soft" - } ] - }"""); - - objectMapper.registerModule(new CustomJacksonModule()); - ScoreAnalysis deserialized = objectMapper.readValue(serialized, ScoreAnalysis.class); - assertThat(deserialized).isEqualTo(originalScoreAnalysis); - } - - @Test - void scoreAnalysisWithMatches() throws JsonProcessingException { - var objectMapper = JsonMapper.builder() - .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .defaultPropertyInclusion( - JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)) - .addModule(TimefoldJacksonModule.createModule()) - .build(); - - var originalScoreAnalysis = getScoreAnalysis(); - var serialized = objectMapper.writeValueAsString(originalScoreAnalysis); - assertThat(serialized) - .isEqualToIgnoringWhitespace(getSerializedScoreAnalysis()); - - objectMapper.registerModule(new CustomJacksonModule()); - ScoreAnalysis deserialized = objectMapper.readValue(serialized, ScoreAnalysis.class); - assertThat(deserialized).isEqualTo(originalScoreAnalysis); - } - - private static ScoreAnalysis getScoreAnalysis() { - var constraintRef1 = ConstraintRef.of("constraint1"); - var constraintRef2 = ConstraintRef.of("constraint2"); - var matchAnalysis1 = new MatchAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), - DefaultConstraintJustification.of(HardSoftScore.ofHard(1), "A", "B")); - var matchAnalysis2 = new MatchAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), - DefaultConstraintJustification.of(HardSoftScore.ofHard(1), "B", "C", "D")); - var matchAnalysis3 = new MatchAnalysis<>(constraintRef2, HardSoftScore.ofSoft(1), - DefaultConstraintJustification.of(HardSoftScore.ofSoft(1), "D")); - var matchAnalysis4 = new MatchAnalysis<>(constraintRef2, HardSoftScore.ofSoft(3), - DefaultConstraintJustification.of(HardSoftScore.ofSoft(3), "A", "C")); - var constraintAnalysis1 = - new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), HardSoftScore.ofHard(2), - List.of(matchAnalysis1, matchAnalysis2)); - var constraintAnalysis2 = - new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(4), - List.of(matchAnalysis3, matchAnalysis4)); - return new ScoreAnalysis<>(HardSoftScore.of(2, 4), - Map.of(constraintRef1, constraintAnalysis1, - constraintRef2, constraintAnalysis2)); - } - - private static String getSerializedScoreAnalysis() { - return """ - { - "score" : "2hard/4soft", - "initialized" : true, - "constraints" : [ { - "name" : "constraint1", - "weight" : "1hard/0soft", - "score" : "2hard/0soft", - "matches" : [ { - "score" : "1hard/0soft", - "justification" : [ "A", "B" ] - }, { - "score" : "1hard/0soft", - "justification" : [ "B", "C", "D" ] - } - ], - "matchCount" : 2 - }, { - "name" : "constraint2", - "weight" : "0hard/1soft", - "score" : "0hard/4soft", - "matches" : [ { - "score" : "0hard/1soft", - "justification" : [ "D" ] - }, { - "score" : "0hard/3soft", - "justification" : [ "A", "C" ] - } ], - "matchCount" : 2 - } ] - }"""; - } - - @Test - void recommendedAssignment() throws JsonProcessingException { - var objectMapper = JsonMapper.builder() - .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .defaultPropertyInclusion( - JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)) - .addModule(TimefoldJacksonModule.createModule()) - .build(); - - var proposition = new Pair<>("A", "1"); - var originalScoreAnalysis = getScoreAnalysis(); - var originalRecommendedAssignment = new DefaultRecommendedAssignment<>(0, proposition, originalScoreAnalysis); - var fitList = List.of(originalRecommendedAssignment); - - var serialized = objectMapper.writeValueAsString(fitList); - assertThat(serialized) - .isEqualToIgnoringWhitespace(""" - [ { - "proposition" : { - "key" : "A", - "value" : "1" - }, - "scoreDiff" : %s - } ]""".formatted(getSerializedScoreAnalysis())); - - objectMapper.registerModule(new CustomJacksonModule()); - List, HardSoftScore>> deserialized = - objectMapper.readValue(serialized, - TypeFactory.defaultInstance().constructCollectionType(List.class, RecommendedAssignment.class)); - assertThat(deserialized) - .hasSize(1) - .first() - .isEqualTo(originalRecommendedAssignment); - } - @Test void constraintWeightOverrides() throws JsonProcessingException { var objectMapper = JsonMapper.builder() @@ -248,8 +80,6 @@ public static final class CustomJacksonModule extends SimpleModule { public CustomJacksonModule() { super("Timefold Custom"); - addDeserializer(ScoreAnalysis.class, new CustomScoreAnalysisJacksonDeserializer()); - addDeserializer(RecommendedAssignment.class, new CustomRecommendedAssignmentJacksonDeserializer()); addDeserializer(ConstraintWeightOverrides.class, new CustomConstraintWeightOverridesDeserializer()); } @@ -265,38 +95,6 @@ protected HardSoftScore parseScore(String scoreString) { } - public static final class CustomScoreAnalysisJacksonDeserializer - extends AbstractScoreAnalysisJacksonDeserializer { - - @Override - protected HardSoftScore parseScore(String scoreString) { - return HardSoftScore.parseScore(scoreString); - } - - @Override - protected ConstraintJustification_ - parseConstraintJustification(ConstraintRef constraintRef, String constraintJustificationString, - HardSoftScore score) { - List justificationList = Arrays.stream(constraintJustificationString.split(",")) - .map(s -> s.replace("[", "") - .replace("]", "") - .replace("\"", "") - .strip()) - .collect(Collectors.toList()); - return (ConstraintJustification_) DefaultConstraintJustification.of(score, justificationList); - } - - } - - public static final class CustomRecommendedAssignmentJacksonDeserializer - extends AbstractRecommendedAssignmentJacksonDeserializer, HardSoftScore> { - - @Override - protected Class> getPropositionClass() { - return (Class) Pair.class; - } - } - public static class TestTimefoldJacksonModuleWrapper { private BendableScore bendableScore; diff --git a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/diff/PlanningSolutionDiffTest.java b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/diff/PlanningSolutionDiffTest.java deleted file mode 100644 index ed7a5fd31b4..00000000000 --- a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/diff/PlanningSolutionDiffTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package ai.timefold.solver.quarkus.jackson.diff; - -import static org.assertj.core.api.Assertions.assertThat; - -import ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeSolution; -import ai.timefold.solver.quarkus.jackson.TimefoldJacksonModule; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; - -class PlanningSolutionDiffTest { - - @Test - void serialize() throws JsonProcessingException { - var oldSolution = TestdataEqualsByCodeSolution.generateSolution("A", 2, 2); - oldSolution.getEntityList().get(0).setValue(oldSolution.getValueList().get(1)); - var newSolution = TestdataEqualsByCodeSolution.generateSolution("B", 3, 3); - var solutionDescriptor = TestdataEqualsByCodeSolution.buildSolutionDescriptor(); - var diff = solutionDescriptor.diff(oldSolution, newSolution); - - var objectMapper = JsonMapper.builder() - .addModule(TimefoldJacksonModule.createModule()) - .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .build(); - - var serialized = objectMapper.writeValueAsString(diff); - assertThat(serialized) - .isEqualToIgnoringWhitespace(""" - { - "added_entities": [ - { - "code": "Generated Entity 2", - "value": { - "code": "Generated Value 2" - } - } - ], - "entity_diffs": [ - { - "entity": { - "code": "Generated Entity 0", - "value": { - "code": "Generated Value 1" - } - }, - "entity_class": "ai.timefold.solver.core.testdomain.equals.TestdataEqualsByCodeEntity", - "variable_diffs": [ - { - "name": "value", - "new_value": { - "code": "Generated Value 0" - }, - "old_value": { - "code": "Generated Value 1" - } - } - ] - } - ], - "removed_entities": [] - }"""); - - } - -} diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java index a4c4b628494..954a044cf7d 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java @@ -30,9 +30,8 @@ public interface SolverBuildTimeConfig { /** * Enable the Nearby Selection quick configuration. *

- * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. + * Note: this setting is only available in Timefold Solver + * Enterprise Edition. */ // Build time - visited by SolverConfig.visitReferencedClasses // which generates the constructor used by Quarkus @@ -48,9 +47,8 @@ public interface SolverBuildTimeConfig { /** * If constraint profiling is enabled. Defaults to false. *

- * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. + * Note: this setting is only available in Timefold Solver + * Enterprise Edition. */ Optional constraintStreamProfilingEnabled(); @@ -61,9 +59,8 @@ public interface SolverBuildTimeConfig { * will no longer be triggered. * Defaults to "false". *

- * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. + * Note: this setting is only available in Timefold Solver + * Enterprise Edition. */ // Build time - modifies the ConstraintProvider class if set Optional constraintStreamAutomaticNodeSharing(); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java index 558523a10c6..1dc159a7737 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java @@ -38,6 +38,7 @@ import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -170,45 +171,27 @@ public static class DummyEasyScoreCalculator } } + @NullMarked public static class DummyIncrementalScoreCalculator implements IncrementalScoreCalculator { - @Override - public void resetWorkingSolution(@NonNull TestdataSolution workingSolution) { - // Ignore - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - // Ignore - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { + public void resetWorkingSolution(TestdataSolution workingSolution) { // Ignore } @Override - public void beforeEntityRemoved(@NonNull Object entity) { + public void beforeVariableChanged(Object entity, String variableName) { // Ignore } @Override - public void afterEntityRemoved(@NonNull Object entity) { + public void afterVariableChanged(Object entity, String variableName) { // Ignore } @Override - public @NonNull SimpleScore calculateScore() { + public SimpleScore calculateScore() { return null; } } diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusIncrementalScoreCalculator.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusIncrementalScoreCalculator.java index 96e50bae971..6000b985578 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusIncrementalScoreCalculator.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusIncrementalScoreCalculator.java @@ -1,49 +1,31 @@ package ai.timefold.solver.quarkus.testdomain.dummy; -import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; -public class DummyTestdataQuarkusIncrementalScoreCalculator implements IncrementalScoreCalculator { +@NullMarked +public class DummyTestdataQuarkusIncrementalScoreCalculator + implements IncrementalScoreCalculator { @Override - public void resetWorkingSolution(@NonNull Object workingSolution) { + public void resetWorkingSolution(Object workingSolution) { // Ignore } @Override - public void beforeEntityAdded(@NonNull Object entity) { + public void beforeVariableChanged(Object entity, String variableName) { // Ignore } @Override - public void afterEntityAdded(@NonNull Object entity) { + public void afterVariableChanged(Object entity, String variableName) { // Ignore } @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull Score calculateScore() { + public SimpleScore calculateScore() { return null; } } diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator.java index 9ebf133e877..aacea3f57cc 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/dummy/DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator.java @@ -1,49 +1,31 @@ package ai.timefold.solver.quarkus.testdomain.dummy; -import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; -public class DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator implements IncrementalScoreCalculator { +@NullMarked +public class DummyTestdataQuarkusShadowVariableIncrementalScoreCalculator + implements IncrementalScoreCalculator { @Override - public void resetWorkingSolution(@NonNull Object workingSolution) { + public void resetWorkingSolution(Object workingSolution) { // Ignore } @Override - public void beforeEntityAdded(@NonNull Object entity) { + public void beforeVariableChanged(Object entity, String variableName) { // Ignore } @Override - public void afterEntityAdded(@NonNull Object entity) { + public void afterVariableChanged(Object entity, String variableName) { // Ignore } @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { - // Ignore - } - - @Override - public void beforeEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public void afterEntityRemoved(@NonNull Object entity) { - // Ignore - } - - @Override - public @NonNull Score calculateScore() { + public SimpleScore calculateScore() { return null; } } diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java index 8507f1f371a..787f13d90bd 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java @@ -29,9 +29,8 @@ public interface SolverRuntimeConfig { Optional daemon(); /** - * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. + * Note: this setting is only available in Timefold Solver + * Enterprise Edition. * Enable multithreaded solving for a single problem, which increases CPU consumption. * Defaults to {@value SolverConfig#MOVE_THREAD_COUNT_NONE}. * Other options include {@value SolverConfig#MOVE_THREAD_COUNT_AUTO}, a number diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIProperties.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIProperties.java index 84beaf9365a..06b27eae692 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIProperties.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIProperties.java @@ -2,7 +2,7 @@ import java.util.List; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; public class TimefoldDevUIProperties { // TODO make record? diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java index 3f12e15d2dc..f5f3e50ed74 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java @@ -13,8 +13,8 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java index 69d2e33f74a..028006d5d5c 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.TreeSet; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -31,9 +32,8 @@ public class SolverProperties { private Boolean daemon; /** - * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. + * Note: this setting is only available in Timefold Solver + * Enterprise Edition. * Enable multithreaded solving for a single problem, which increases CPU consumption. * Defaults to "NONE". * Other options include "AUTO", a number or formula based on the available processor count. @@ -50,12 +50,11 @@ public class SolverProperties { private Boolean constraintStreamProfilingEnabled; /** - * Note: this setting is only available - * for Timefold Solver - * Enterprise Edition. - * Enable rewriting the {@link ai.timefold.solver.core.api.score.stream.ConstraintProvider} class + * Note: this setting is only available in Timefold Solver + * Enterprise Edition. + * Enable rewriting the {@link ConstraintProvider} class * so nodes share lambdas when possible, improving performance. - * When enabled, breakpoints placed in the {@link ai.timefold.solver.core.api.score.stream.ConstraintProvider} + * When enabled, breakpoints placed in the {@link ConstraintProvider} * will no longer be triggered. * Defaults to "false". */ diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/basic/constraints/incremental/DummySpringIncrementalScore.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/basic/constraints/incremental/DummySpringIncrementalScore.java index 947f626bbb3..19e321199b8 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/basic/constraints/incremental/DummySpringIncrementalScore.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/basic/constraints/incremental/DummySpringIncrementalScore.java @@ -1,48 +1,31 @@ package ai.timefold.solver.spring.boot.autoconfigure.dummy.basic.constraints.incremental; -import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; -public class DummySpringIncrementalScore implements IncrementalScoreCalculator { - @Override - public void resetWorkingSolution(@NonNull Object workingSolution) { - - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - - } +@NullMarked +public class DummySpringIncrementalScore + implements IncrementalScoreCalculator { @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { + public void resetWorkingSolution(Object workingSolution) { } @Override - public void beforeEntityRemoved(@NonNull Object entity) { + public void beforeVariableChanged(Object entity, String variableName) { } @Override - public void afterEntityRemoved(@NonNull Object entity) { + public void afterVariableChanged(Object entity, String variableName) { } @Override - public @NonNull Score calculateScore() { + public SimpleScore calculateScore() { return null; } } diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/list/constraints/incremental/DummySpringListIncrementalScore.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/list/constraints/incremental/DummySpringListIncrementalScore.java index c7770c235a9..915e519dcc3 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/list/constraints/incremental/DummySpringListIncrementalScore.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/list/constraints/incremental/DummySpringListIncrementalScore.java @@ -1,48 +1,31 @@ package ai.timefold.solver.spring.boot.autoconfigure.dummy.list.constraints.incremental; -import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; -public class DummySpringListIncrementalScore implements IncrementalScoreCalculator { - @Override - public void resetWorkingSolution(@NonNull Object workingSolution) { - - } - - @Override - public void beforeEntityAdded(@NonNull Object entity) { - - } - - @Override - public void afterEntityAdded(@NonNull Object entity) { - - } - - @Override - public void beforeVariableChanged(@NonNull Object entity, @NonNull String variableName) { - - } +@NullMarked +public class DummySpringListIncrementalScore + implements IncrementalScoreCalculator { @Override - public void afterVariableChanged(@NonNull Object entity, @NonNull String variableName) { + public void resetWorkingSolution(Object workingSolution) { } @Override - public void beforeEntityRemoved(@NonNull Object entity) { + public void beforeVariableChanged(Object entity, String variableName) { } @Override - public void afterEntityRemoved(@NonNull Object entity) { + public void afterVariableChanged(Object entity, String variableName) { } @Override - public @NonNull Score calculateScore() { + public SimpleScore calculateScore() { return null; } } diff --git a/tools/benchmark-aggregator/src/test/java/ai/timefold/solver/benchmark/aggregator/PlannerBenchmarkResultTest.java b/tools/benchmark-aggregator/src/test/java/ai/timefold/solver/benchmark/aggregator/PlannerBenchmarkResultTest.java index ec0b7bd9b5c..dcc7831113a 100644 --- a/tools/benchmark-aggregator/src/test/java/ai/timefold/solver/benchmark/aggregator/PlannerBenchmarkResultTest.java +++ b/tools/benchmark-aggregator/src/test/java/ai/timefold/solver/benchmark/aggregator/PlannerBenchmarkResultTest.java @@ -11,7 +11,7 @@ import ai.timefold.solver.benchmark.impl.report.BenchmarkReportFactory; import ai.timefold.solver.benchmark.impl.result.BenchmarkResultIO; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -45,15 +45,25 @@ void xmlReadBenchmarkResultAggregated() throws URISyntaxException, IOException { assertThat(aggregatedPlannerBenchmarkResult.getFailureCount()).isZero(); } - // nested class below are used in the testPlannerBenchmarkResult.xml + // nested classes below are used in the testPlannerBenchmarkResult.xml - private static abstract class DummyIncrementalScoreCalculator - implements IncrementalScoreCalculator { + public static class DummyEasyScoreCalculator + implements EasyScoreCalculator { + @Override + public SimpleScore calculateScore(TestdataSolution solution) { + return null; + } } - private static abstract class DummyDistanceNearbyMeter + public static class DummyDistanceNearbyMeter implements NearbyDistanceMeter { + @Override + public double getNearbyDistance(TestdataSolution origin, TestdataEntity destination) { + return 0; + } + } + } diff --git a/tools/benchmark-aggregator/src/test/resources/ai/timefold/solver/benchmark/aggregator/testPlannerBenchmarkResult.xml b/tools/benchmark-aggregator/src/test/resources/ai/timefold/solver/benchmark/aggregator/testPlannerBenchmarkResult.xml index ea89b5a2fee..f8a09cb4b98 100644 --- a/tools/benchmark-aggregator/src/test/resources/ai/timefold/solver/benchmark/aggregator/testPlannerBenchmarkResult.xml +++ b/tools/benchmark-aggregator/src/test/resources/ai/timefold/solver/benchmark/aggregator/testPlannerBenchmarkResult.xml @@ -18,7 +18,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -66,7 +66,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -114,7 +114,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -170,7 +170,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -226,7 +226,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -242,7 +242,7 @@ - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyDistanceNearbyMeter 40 @@ -304,7 +304,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.aggregator.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java index b4d053b3836..7b16790bdca 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java @@ -6,9 +6,11 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.api.solver.SolutionUpdatePolicy; import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.solver.DefaultSolver; import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; @@ -123,11 +125,12 @@ public SubSingleBenchmarkRunner call() { var isConstraintMatchEnabled = solver.getSolverScope().getScoreDirector().getConstraintMatchPolicy() .isEnabled(); if (isConstraintMatchEnabled) { // Easy calculator fails otherwise. - var scoreExplanation = - solutionManager.explain(solution, SolutionUpdatePolicy.NO_UPDATE); - subSingleBenchmarkResult.setScoreExplanationSummary(scoreExplanation.getSummary()); + var scoreExplanation = TimefoldSolverEnterpriseService.loadOrNull(b -> solutionManager.analyze(solution, + ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT, SolutionUpdatePolicy.NO_UPDATE)); + if (scoreExplanation != null) { // Avoid hard fail when Enterprise is not present. + subSingleBenchmarkResult.setScoreExplanationSummary(scoreExplanation.summarize()); + } } - problemBenchmarkResult.writeSolution(subSingleBenchmarkResult, solution); } MDC.remove(NAME_MDC); diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ConstraintSummary.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ConstraintSummary.java index bdcf61df42f..8120af90a86 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ConstraintSummary.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ConstraintSummary.java @@ -1,7 +1,7 @@ package ai.timefold.solver.benchmark.impl.statistic; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; public record ConstraintSummary>(ConstraintRef constraintRef, Score_ score, int count) { diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java index 4bb4f3628d5..650c19a20a6 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java @@ -12,7 +12,7 @@ import java.util.function.ObjLongConsumer; import java.util.stream.Collectors; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreStatisticPoint.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreStatisticPoint.java index 9de3fef373e..3d0a6228f0a 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreStatisticPoint.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreStatisticPoint.java @@ -2,7 +2,7 @@ import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; public class ConstraintMatchTotalBestScoreStatisticPoint extends StatisticPoint { diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatistic.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatistic.java index 884e160b299..5936ef06146 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatistic.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatistic.java @@ -9,7 +9,7 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.PureSubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreStatisticPoint.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreStatisticPoint.java index 87b1dc6b2fb..78cf8cf229f 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreStatisticPoint.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreStatisticPoint.java @@ -2,7 +2,7 @@ import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; public class ConstraintMatchTotalStepScoreStatisticPoint extends StatisticPoint { diff --git a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatistic.java b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatistic.java index 61f35cb45ae..9afbed266d9 100644 --- a/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatistic.java +++ b/tools/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatistic.java @@ -9,7 +9,7 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.PureSubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; diff --git a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java index 14f87d29d15..cd996f5c971 100644 --- a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java +++ b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/result/PlannerBenchmarkResultTest.java @@ -13,7 +13,7 @@ import ai.timefold.solver.benchmark.impl.loader.FileProblemProvider; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.random.RandomType; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -127,10 +127,10 @@ void xmlReportRemainsSameAfterReadWrite() throws IOException { assertThat(jaxbString.trim()).isEqualToIgnoringWhitespace(originalXml.trim()); } - // nested class below are used in the testPlannerBenchmarkResult.xml + // nested classes below are used in the testPlannerBenchmarkResult.xml - private static abstract class DummyIncrementalScoreCalculator - implements IncrementalScoreCalculator { + private static abstract class DummyEasyScoreCalculator + implements EasyScoreCalculator { } diff --git a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatisticTest.java b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatisticTest.java index 34afac64f3d..b32e71489e4 100644 --- a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatisticTest.java +++ b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalbestscore/ConstraintMatchTotalBestScoreSubSingleStatisticTest.java @@ -8,7 +8,7 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.testdomain.TestdataSolution; import org.assertj.core.api.SoftAssertions; diff --git a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatisticTest.java b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatisticTest.java index 43c6d2de339..6aea13a81a9 100644 --- a/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatisticTest.java +++ b/tools/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/constraintmatchtotalstepscore/ConstraintMatchTotalStepScoreSubSingleStatisticTest.java @@ -8,7 +8,7 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.constraint.ConstraintRef; +import ai.timefold.solver.core.api.score.stream.ConstraintRef; import ai.timefold.solver.core.testdomain.TestdataSolution; import org.assertj.core.api.SoftAssertions; diff --git a/tools/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml b/tools/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml index 1d548684c36..c8bbbc830ab 100644 --- a/tools/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml +++ b/tools/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml @@ -18,7 +18,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -66,7 +66,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -114,7 +114,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -170,7 +170,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -226,7 +226,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN @@ -304,7 +304,7 @@ ai.timefold.solver.core.testdomain.TestdataSolution ai.timefold.solver.core.testdomain.TestdataEntity - ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyIncrementalScoreCalculator + ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResultTest$DummyEasyScoreCalculator ONLY_DOWN diff --git a/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipe.java b/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipe.java index 0b99022d30a..0d1de0968b9 100644 --- a/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipe.java +++ b/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipe.java @@ -101,6 +101,15 @@ public List getRecipeList() { new RemoveFieldFromMethodInvocationRecipe( "ai.timefold.solver.core.config.solver.SolverConfig getDomainAccessType()"), new RemoveFieldFromMethodInvocationRecipe( - "ai.timefold.solver.core.config.solver.SolverConfig determineDomainAccessType()")); + "ai.timefold.solver.core.config.solver.SolverConfig determineDomainAccessType()"), + // indictWith + new RemoveMethodInvocations( + "ai.timefold.solver.core.api.score.stream.uni.UniConstraintBuilder indictWith(..)"), + new RemoveMethodInvocations( + "ai.timefold.solver.core.api.score.stream.bi.BiConstraintBuilder indictWith(..)"), + new RemoveMethodInvocations( + "ai.timefold.solver.core.api.score.stream.tri.TriConstraintBuilder indictWith(..)"), + new RemoveMethodInvocations( + "ai.timefold.solver.core.api.score.stream.quad.QuadConstraintBuilder indictWith(..)")); } } diff --git a/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipe.java b/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipe.java index f3ebf705385..8c36d49abe3 100644 --- a/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipe.java +++ b/tools/migration/src/main/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipe.java @@ -51,28 +51,31 @@ public List getRecipeList() { true), // Jackson API new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.bendablebigdecimal", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.bendable", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.hardmediumsoftbigdecimal", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.hardmediumsoft", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.hardsoftbigdecimal", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.hardsoft", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.simplebigdecimal", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), new ChangePackage("ai.timefold.solver.jackson.api.score.buildin.simple", - "ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", + true), + new ChangePackage("ai.timefold.solver.jackson.api.score.buildin", + "ai.timefold.solver.jackson.api.score", true), // JAXB API new ChangePackage("ai.timefold.solver.jaxb.api.score.buildin.bendablebigdecimal", @@ -124,6 +127,9 @@ public List getRecipeList() { new ChangePackage("ai.timefold.solver.quarkus.jackson.score.buildin.simple", "ai.timefold.solver.quarkus.jackson.score", true), + new ChangePackage("ai.timefold.solver.quarkus.jackson.score.buildin", + "ai.timefold.solver.quarkus.jackson.score", + true), // Value Range API new ChangePackage("ai.timefold.solver.core.impl.domain.valuerange.buildin.bigdecimal", "ai.timefold.solver.core.impl.domain.valuerange", true), diff --git a/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipeTest.java b/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipeTest.java index f6a7539c027..327433f343d 100644 --- a/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipeTest.java +++ b/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralMethodDeleteInvocationMigrationRecipeTest.java @@ -90,6 +90,28 @@ public interface Constraint { String getConstraintPackage(); String getConstraintId(); ConstraintFactory getConstraintFactory(); + }""", + """ + package ai.timefold.solver.core.api.score.stream.uni; + import java.util.function.Function; + public interface UniConstraintBuilder { + UniConstraintBuilder indictWith(Function indictment); + }""", + """ + package ai.timefold.solver.core.api.score.stream.bi; + import java.util.function.BiFunction; + public interface BiConstraintBuilder { + BiConstraintBuilder indictWith(BiFunction indictment); + }""", + """ + package ai.timefold.solver.core.api.score.stream.tri; + public interface TriConstraintBuilder { + TriConstraintBuilder indictWith(Object indictment); + }""", + """ + package ai.timefold.solver.core.api.score.stream.quad; + public interface QuadConstraintBuilder { + QuadConstraintBuilder indictWith(Object indictment); }""")); } @@ -272,6 +294,49 @@ public void test() { }""")); } + @Test + void removeIndictWith() { + rewriteRun(java( + """ + package timefold; + + import ai.timefold.solver.core.api.score.stream.uni.UniConstraintBuilder; + import ai.timefold.solver.core.api.score.stream.bi.BiConstraintBuilder; + import ai.timefold.solver.core.api.score.stream.tri.TriConstraintBuilder; + import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintBuilder; + import java.util.function.Function; + + public class Test { + UniConstraintBuilder uni; + BiConstraintBuilder bi; + TriConstraintBuilder tri; + QuadConstraintBuilder quad; + public void test() { + uni.indictWith(Function.identity()); + bi.indictWith((a, b) -> a); + tri.indictWith(null); + quad.indictWith(null); + } + }""", + """ + package timefold; + + import ai.timefold.solver.core.api.score.stream.uni.UniConstraintBuilder; + import ai.timefold.solver.core.api.score.stream.bi.BiConstraintBuilder; + import ai.timefold.solver.core.api.score.stream.tri.TriConstraintBuilder; + import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintBuilder; + import java.util.function.Function; + + public class Test { + UniConstraintBuilder uni; + BiConstraintBuilder bi; + TriConstraintBuilder tri; + QuadConstraintBuilder quad; + public void test() { + } + }""")); + } + @Test void removeSolverConfigMethods() { rewriteRun(java( diff --git a/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipeTest.java b/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipeTest.java index 71b6b6c06a3..f377d1388b7 100644 --- a/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipeTest.java +++ b/tools/migration/src/test/java/ai/timefold/solver/migration/v2/GeneralPackageRenameMigrationRecipeTest.java @@ -35,6 +35,7 @@ public void defaults(RecipeSpec spec) { "package ai.timefold.solver.jpa.api.score.buildin.simplebigdecimal; public class SimpleBigDecimalScoreConverter {}", "package ai.timefold.solver.jpa.api.score.buildin.simple; public class SimpleScoreConverter {}", // Jackson API + "package ai.timefold.solver.jackson.api.score.buildin; public class AbstractScoreJacksonSerializer {}", "package ai.timefold.solver.jackson.api.score.buildin.bendablebigdecimal; public class BendableBigDecimalScoreJacksonDeserializer {}", "package ai.timefold.solver.jackson.api.score.buildin.bendable; public class BendableScoreJacksonDeserializer {}", "package ai.timefold.solver.jackson.api.score.buildin.hardmediumsoftbigdecimal; public class HardMediumSoftBigDecimalScoreJacksonDeserializer {}", @@ -52,7 +53,8 @@ public void defaults(RecipeSpec spec) { "package ai.timefold.solver.jaxb.api.score.buildin.hardsoft; public class HardSoftScoreJaxbAdapter {}", "package ai.timefold.solver.jaxb.api.score.buildin.simplebigdecimal; public class SimpleBigDecimalScoreJaxbAdapter {}", "package ai.timefold.solver.jaxb.api.score.buildin.simple; public class SimpleScoreJaxbAdapter {}", - // Jackson API + // Quarkus Jackson API + "package ai.timefold.solver.quarkus.jackson.score.buildin; public class AbstractScoreJacksonSerializer {}", "package ai.timefold.solver.quarkus.jackson.score.buildin.bendablebigdecimal; public class BendableBigDecimalScoreJacksonDeserializer {}", "package ai.timefold.solver.quarkus.jackson.score.buildin.bendable; public class BendableScoreJacksonDeserializer {}", "package ai.timefold.solver.quarkus.jackson.score.buildin.hardmediumsoftbigdecimal; public class HardMediumSoftBigDecimalScoreJacksonDeserializer {}", @@ -73,7 +75,7 @@ public void defaults(RecipeSpec spec) { } @Test - void migratePersistence() { + void migrateJpa() { rewriteRun(java( """ package timefold; @@ -131,6 +133,7 @@ void migrateJackson() { """ package timefold; + import ai.timefold.solver.jackson.api.score.buildin.AbstractScoreJacksonDeserializer; import ai.timefold.solver.jackson.api.score.buildin.bendablebigdecimal.BendableBigDecimalScoreJacksonDeserializer; import ai.timefold.solver.jackson.api.score.buildin.bendable.BendableScoreJacksonDeserializer; import ai.timefold.solver.jackson.api.score.buildin.hardmediumsoftbigdecimal.HardMediumSoftBigDecimalScoreJacksonDeserializer; @@ -141,6 +144,7 @@ void migrateJackson() { import ai.timefold.solver.jackson.api.score.buildin.simple.SimpleScoreJacksonDeserializer; public class Test { + AbstractScoreJacksonDeserializer abstractScoreJacksonDeserializer; BendableBigDecimalScoreJacksonDeserializer bendableBigDecimalScoreJacksonDeserializer; BendableScoreJacksonDeserializer bendableScoreJacksonDeserializer; HardMediumSoftBigDecimalScoreJacksonDeserializer hardMediumSoftBigDecimalScoreJacksonDeserializer; @@ -153,16 +157,18 @@ public class Test { """ package timefold; - import ai.timefold.solver.jackson.api.score.buildin.BendableBigDecimalScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.BendableScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftBigDecimalScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.HardMediumSoftScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.HardSoftBigDecimalScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.HardSoftScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.SimpleBigDecimalScoreJacksonDeserializer; - import ai.timefold.solver.jackson.api.score.buildin.SimpleScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.AbstractScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.BendableBigDecimalScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.BendableScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.HardMediumSoftBigDecimalScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.HardMediumSoftScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.HardSoftBigDecimalScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.HardSoftScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.SimpleBigDecimalScoreJacksonDeserializer; + import ai.timefold.solver.jackson.api.score.SimpleScoreJacksonDeserializer; public class Test { + AbstractScoreJacksonDeserializer abstractScoreJacksonDeserializer; BendableBigDecimalScoreJacksonDeserializer bendableBigDecimalScoreJacksonDeserializer; BendableScoreJacksonDeserializer bendableScoreJacksonDeserializer; HardMediumSoftBigDecimalScoreJacksonDeserializer hardMediumSoftBigDecimalScoreJacksonDeserializer; @@ -229,6 +235,7 @@ void migrateQuarkusJackson() { """ package timefold; + import ai.timefold.solver.quarkus.jackson.score.buildin.AbstractScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.buildin.bendablebigdecimal.BendableBigDecimalScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.buildin.bendable.BendableScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.buildin.hardmediumsoftbigdecimal.HardMediumSoftBigDecimalScoreJacksonDeserializer; @@ -239,6 +246,7 @@ void migrateQuarkusJackson() { import ai.timefold.solver.quarkus.jackson.score.buildin.simple.SimpleScoreJacksonDeserializer; public class Test { + AbstractScoreJacksonDeserializer abstractScoreJacksonDeserializer; BendableBigDecimalScoreJacksonDeserializer bendableBigDecimalScoreJacksonDeserializer; BendableScoreJacksonDeserializer bendableScoreJacksonDeserializer; HardMediumSoftBigDecimalScoreJacksonDeserializer hardMediumSoftBigDecimalScoreJacksonDeserializer; @@ -251,6 +259,7 @@ public class Test { """ package timefold; + import ai.timefold.solver.quarkus.jackson.score.AbstractScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.BendableBigDecimalScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.BendableScoreJacksonDeserializer; import ai.timefold.solver.quarkus.jackson.score.HardMediumSoftBigDecimalScoreJacksonDeserializer; @@ -261,6 +270,7 @@ public class Test { import ai.timefold.solver.quarkus.jackson.score.SimpleScoreJacksonDeserializer; public class Test { + AbstractScoreJacksonDeserializer abstractScoreJacksonDeserializer; BendableBigDecimalScoreJacksonDeserializer bendableBigDecimalScoreJacksonDeserializer; BendableScoreJacksonDeserializer bendableScoreJacksonDeserializer; HardMediumSoftBigDecimalScoreJacksonDeserializer hardMediumSoftBigDecimalScoreJacksonDeserializer;