Constraint Solver
Kairos Constraint
High-performance constraint-satisfaction solver. Places entities on an N-dimensional grid or time axis, satisfies hard rules and minimises soft penalties.
Pricing
Once per request, regardless of duration or result.
/v1/kairos/constraintSolves a constraint satisfaction problem. The full solution is available via a presigned URL.
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
dimensions | array<Dimension> | Yes | — | Variable axes (≥ 1). Each entry defines one dimension with a unique id — see Dimensions section. |
entities | array<Entity> | Yes | — | What gets placed (≥ 1). Each entity has min_placements / max_placements and an optional per-dimension restriction — see Entities section. |
constraints | array<Constraint> | Optional | [] | Rules the solution must satisfy (hard) or pays penalty for violating (soft). 15 types available — see Constraints section. |
config | Config | Optional | {} | Solver settings: time budget, early stopping, correlation id, URL validity — see Config section. |
Request
curl -X POST "https://api.centra.dbpg.io/v1/kairos/constraint" \
-H "Authorization: Bearer sk-live-your_api_key" \
-H "Content-Type: application/json" \
-d '{
"dimensions": [
{ "type": "range", "id": "slot", "size": 6, "ordered": true },
{ "type": "set", "id": "room", "values": ["main", "a", "b"] }
],
"entities": [
{ "id": "keynote",
"dimensions": { "slot": { "values": [0] }, "room": { "values": ["main"] } },
"min_placements": 1, "max_placements": 1 },
{ "id": "talk_a",
"dimensions": { "slot": {}, "room": { "values": ["a", "b"] } },
"min_placements": 1, "max_placements": 1 }
],
"constraints": [
{ "type": "not_same_slot", "hard": true,
"entity_a": "talk_a", "entity_b": "keynote", "dimension": "slot" }
],
"config": { "time_limit_seconds": 60, "request_id": "example-1" }
}'Response
{
"status": "optimal",
"request_id": "example-1",
"warnings": [],
"statistics": {
"timing": { "model_build_seconds": 0.01, "solve_seconds": 0.03, "total_seconds": 0.04 },
"problem_size": { "dimensions": 2, "entities": 2, "constraints": 1, "variables": 18, "cells": 18 },
"total_penalty": 0, "best_bound": 0, "gap_percent": 0.0,
"soft_constraint_violations": []
},
"result_url": "https://...s3.eu-central-1.amazonaws.com/csp/example-1.json?...",
"result_expires_at": "2026-04-24T17:00:00+00:00",
"usage": {
"credits_used": 2300,
"remaining_balance": 7700
}
}Dimensions
Each dimension is an axis over which entities take values. Three types. Every dimension needs a unique id within the request.
set — Categorical values
List of discrete named values. Good for rooms, teams, products, colours — anything where values have no inherent numerical order.
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
type | "set" | Yes | — | Discriminator, must be exactly "set". |
id | string | Yes | — | Unique dimension identifier, referenced by entities and constraints. |
values | array<string> | Yes | — | Allowed string values. Order only matters when ordered=true. |
ordered | boolean | Optional | false | Default false. When true, values are treated as sorted — relevant for sequence, proximity, spacing. |
{ "type": "set", "id": "room", "values": ["r1", "r2", "r3"], "ordered": false }range — Numeric range
Integer values from 0 to size-1. Ideal for time slots, numbers, ring positions — anywhere ordering is semantically relevant.
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
type | "range" | Yes | — | Discriminator, must be exactly "range". |
id | string | Yes | — | Unique dimension identifier. |
size | integer ≥ 1 | Yes | — | Number of values. Allowed values are integers 0..size-1. |
ordered | boolean | Optional | true | Default true. range dimensions are ordered by default. |
{ "type": "range", "id": "slot", "size": 40, "ordered": true }interval — Continuous time axis
Time dimension with configurable duration. Internally the solver creates an IntervalVar with start/duration/end per entity; NoOverlap per resource group is automatic.
Parameter
| Name | Type | Status | Description |
|---|---|---|---|
type | "interval" | Yes | Discriminator, must be exactly "interval". |
id | string | Yes | Unique dimension identifier. |
horizon | integer ≥ 1 | Yes | Total length of the time axis in time units (e.g. 480 = 8 hours in minutes). |
{ "type": "interval", "id": "time", "horizon": 480 }Constraint: maximum one interval dimension per request.
Entities
The objects the solver places — lessons, ad slots, shifts, boxes, etc.
Fields
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
id | string | Yes | — | Unique entity identifier in the request. Referenced by constraints and the response. |
name | string | Optional | "" | Free-text display label for UI and logs. Default empty string. |
dimensions | object | Yes | — | Dict dim_id → EntityDimensionConfig. Each entry specifies which values this entity may use on the given dimension. Missing dimensions mean: all values allowed. |
min_placements | integer ≥ 1 | Yes | — | Minimum number of active placements for this entity (≥ 1). |
max_placements | integer ≥ 1 | Yes | — | Maximum number of active placements (≥ 1). Must be ≥ min_placements. |
EntityDimensionConfig
Per dimension under entities[].dimensions. All fields optional.
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
values | array<string|integer> | Optional | — | Allowed values for this entity on this dimension, subset of the dimension values. Default: all values of the dimension. |
mode | "any" | "all" | Optional | "any" | "any" = entity picks one value per placement. "all" = entity occupies all listed values simultaneously as a block. Default "any". |
size | integer ≥ 1 | Optional | 1 | Number of adjacent values a single placement occupies (≥ 1). Only meaningful on ordered dimensions — e.g. size=2 for a double period. Default 1. |
duration | integer ≥ 1 | Optional | 1 | Only on interval dimensions. Duration of the placement in time units. Default 1. |
{
"id": "math_5a",
"name": "Math 5a with Mueller",
"dimensions": {
"slot": {},
"room": { "values": ["r1", "r2"], "mode": "any", "size": 1 }
},
"min_placements": 1,
"max_placements": 1
}Constraints
Rules applied to the solution. Each rule is either hard (must hold) or soft (may be violated at a penalty cost). The solver minimises the sum of all soft penalties.
Shared fields
All constraint types share these base fields; type-specific fields follow below.
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
type | string | Yes | — | Discriminator. One of the 15 types below. |
id | string | Optional | null | Optional identifier. Appears in response.statistics.soft_constraint_violations[].constraint_id when violated. |
hard | boolean | Yes | — | true = must hold, otherwise infeasible. false = may be violated, costs penalty per violation unit. |
penalty | integer ≥ 1 | Alternative | — | Required when hard=false. Cost per violation unit (integer ≥ 1). |
Overview
All 15 rule types at a glance. The type column links to the detail below.
| # | Type | Mode | Summary | Required fields |
|---|---|---|---|---|
| 1 | fill | hardsoft | Every cell must be occupied. | — |
| 2 | cardinality | hardsoft | Bounds the placement count of an entity. | entityminmax |
| 3 | proximity | hardsoft | Minimum gap between two entities on one dimension. | entity_aentity_bdimensionmin_gap |
| 4 | spacing | soft | Anti-clump — penalises adjacent placements of the same entity. | entitydimensionclump_penalty |
| 5 | same_slot | hardsoft | Two entities must share a dimension value. | entity_aentity_bdimension |
| 6 | not_same_slot | hardsoft | Two entities must not share a dimension value. | entity_aentity_bdimension |
| 7 | sequence | hardsoft | Entity A must come before entity B. | firstthendimension |
| 8 | follows | hardsoft | Entity B must be placed directly after entity A. | entity_aentity_bdimension |
| 9 | not_follows | hardsoft | Entity B must not be placed directly after entity A. | entity_aentity_bdimension |
| 10 | mutual_exclusion | hardsoft | Only one of the listed entities per value. | entitiesdimension |
| 11 | max_per_value | hardsoft | Upper bound on placements of an entity per value. | entitydimensionmax |
| 12 | cumulative | hardsoft | Weighted capacity bound per value. | dimensiondemandsmax_capacity |
| 13 | pairwise_exclusion | hardsoft | At most one entity per combination of several dimensions. | dimensions |
| 14 | pairwise_fill | hardsoft | Every combination of several dimensions must be occupied. | dimensions |
| 15 | expression | hardsoft | Free-form rule in a custom DSL. | rule |
Detailed type reference
fill
Every cell (combination of all dimension values) must be occupied by at least one entity. Classic full-coverage of a grid.
No type-specific fields — only the shared fields above.
Example
{ "type": "fill", "hard": true }cardinality
Limits how often an entity may be placed. For soft: each missing or surplus placement costs penalty.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity | string | Yes | ID of the entity to bound. |
min | integer | Yes | Minimum number of placements. |
max | integer | Yes | Maximum number of placements. |
Example
{
"type": "cardinality",
"hard": false,
"penalty": 5,
"entity": "math_5a",
"min": 4,
"max": 5
}proximity
Two entities must be at least min_gap values apart on the given (ordered) dimension.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity_a | string | Yes | ID of the first entity. |
entity_b | string | Yes | ID of the second entity. |
dimension | string | Yes | Dimension the gap is measured on. Must be ordered. |
min_gap | integer ≥ 1 | Yes | Minimum gap in value steps. |
Example
{
"type": "proximity",
"hard": true,
"entity_a": "brand_A",
"entity_b": "brand_B",
"dimension": "slot",
"min_gap": 3
}spacing
Anti-clump rule: prevents placements of the same entity from sitting in directly adjacent values. Always soft — hard=false is mandatory.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity | string | Yes | ID of the entity whose placements should be spread. |
dimension | string (ordered) | Yes | Ordered dimension on which adjacency is checked. |
clump_penalty | integer ≥ 1 | Yes | Penalty per directly-adjacent pair of the same entity. |
Example
{
"type": "spacing",
"hard": false,
"penalty": 10,
"entity": "lightning",
"dimension": "slot",
"clump_penalty": 50
}same_slot
When entity A is active, at least one placement of B must share the same dimension value. Couples two entities.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity_a | string | Yes | ID of the first entity (triggers the coupling). |
entity_b | string | Yes | ID of the second entity (must match the value). |
dimension | string | Yes | Dimension on which the value equality holds. |
Example
{
"type": "same_slot",
"hard": true,
"entity_a": "a",
"entity_b": "b",
"dimension": "day"
}not_same_slot
Entity A and entity B must not share a dimension value. Decouples two entities.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity_a | string | Yes | ID of the first entity. |
entity_b | string | Yes | ID of the second entity. |
dimension | string | Yes | Dimension on which values must differ. |
Example
{
"type": "not_same_slot",
"hard": true,
"entity_a": "a",
"entity_b": "b",
"dimension": "day"
}sequence
The earliest active placement of first must come before the earliest of then. For ordering dependencies.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
first | string | Yes | ID of the entity that must come first. |
then | string | Yes | ID of the entity that comes after. |
dimension | string (ordered) | Yes | Ordered dimension on which order is defined. |
Example
{
"type": "sequence",
"hard": true,
"first": "intro",
"then": "advanced",
"dimension": "slot"
}follows
When entity A is active at position t, entity B must be active at position t+1. Direct temporal successor.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity_a | string | Yes | ID of the predecessor entity. |
entity_b | string | Yes | ID of the entity that must come immediately after. |
dimension | string (ordered) | Yes | Ordered dimension on which succession is defined. |
Example
{
"type": "follows",
"hard": true,
"entity_a": "a",
"entity_b": "b",
"dimension": "slot"
}not_follows
When entity A is at position t, entity B must not be at t+1. Negative form of follows.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity_a | string | Yes | ID of the predecessor entity. |
entity_b | string | Yes | ID of the entity that must not come immediately after. |
dimension | string (ordered) | Yes | Ordered dimension on which succession is checked. |
Example
{
"type": "not_follows",
"hard": true,
"entity_a": "a",
"entity_b": "b",
"dimension": "slot"
}mutual_exclusion
At most one entity from the listed group may be active per dimension value.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entities | array<string> | Yes | List of entity IDs that mutually exclude each other. |
dimension | string | Yes | Dimension on which the exclusivity applies. |
Example
{
"type": "mutual_exclusion",
"hard": true,
"entities": ["a", "b", "c"],
"dimension": "slot"
}max_per_value
Upper bound on how often a single entity may be placed per dimension value.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
entity | string | Yes | ID of the entity affected. |
dimension | string | Yes | Dimension on which the bound applies. |
max | integer ≥ 1 | Yes | Maximum number of placements per value (≥ 1). |
Example
{
"type": "max_per_value",
"hard": true,
"entity": "brand_x",
"dimension": "slot",
"max": 1
}cumulative
Weighted capacity constraint: the sum of weights of all entities active on any dimension value must not exceed max_capacity. Classic capacitated-resource rule.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
dimension | string | Yes | Dimension on which capacity is measured. |
demands | object<entity_id, integer> | Yes | Dict entity_id → weight. Entities not listed have weight 0. |
max_capacity | integer ≥ 1 | Yes | Capacity bound per dimension value (≥ 1). |
Example
{
"type": "cumulative",
"hard": true,
"dimension": "slot",
"demands": {
"truck_a": 3,
"truck_b": 2,
"truck_c": 4
},
"max_capacity": 6
}pairwise_exclusion
For every combination of the listed dimension values, at most one entity may be active. Generalises cell exclusivity to any dimension subset. Classic: "a teacher can never be in two rooms at the same time" via dimensions=[slot, teacher].
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
dimensions | array<string> (≥ 1) | Yes | List of dimension ids whose combinations are exclusive (≥ 1 element). |
Example
{
"type": "pairwise_exclusion",
"hard": true,
"dimensions": ["slot", "teacher"]
}pairwise_fill
For every combination of the listed dimension values, at least one entity must be active. Generalises fill to any dimension subset.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
dimensions | array<string> (≥ 1) | Yes | List of dimension ids whose combinations must be covered (≥ 1 element). |
Example
{
"type": "pairwise_fill",
"hard": true,
"dimensions": ["slot", "room"]
}expression
Free-form rule in a client-side DSL for special cases. Not content-validated by the verifier — only evaluated by the solver. Only use when none of the other 14 types fit.
Type-specific fields
| Name | Type | Status | Description |
|---|---|---|---|
rule | string (DSL) | Yes | Rule string in custom syntax. |
Example
{
"type": "expression",
"hard": true,
"rule": "<custom DSL>"
}Config
All config fields are optional; schema defaults apply.
Fields
Parameter
| Name | Type | Status | Default | Description |
|---|---|---|---|---|
time_limit_seconds | float (1–600) | Optional | 60 | Maximum time the solver may spend searching. Between 1 and 600 seconds. Default 60. |
stop_after_first_solution | boolean | Optional | false | true = solver returns the first feasible solution and exits. No optimisation beyond that. Default false. |
relative_gap_limit | float (0–1) | null | Optional | null | Stops the solver as soon as the relative gap to the theoretical lower bound is ≤ X. E.g. 0.05 = 5 %. Default null. |
absolute_gap_limit | integer ≥ 0 | null | Optional | null | Stops the solver as soon as the absolute penalty gap is ≤ X. Default null. |
request_id | string | null | Optional | null | Your own correlation id. Echoed 1:1 into the response and appears in CloudWatch logs for cross-system tracing. |
result_url_expires_in | integer (60–604800) | Optional | 10800 | Validity of the presigned URL in seconds. Between 60 and 604800 (7 days). Default 10800 (3 h). |
hints | array<HintAssignment> | Optional | [] | Warm-start hints from a previous solution. Each entry has the fields entity and values. |
Response
For status optimal and feasible the full solution lives behind result_url. The wire-body fields:
Status values
HTTP status code and billing logic per result classification.
| status | HTTP | Billed | Meaning |
|---|---|---|---|
optimal | 200 | Yes | provably optimal solution |
feasible | 200 | Yes | valid solution, optimality not proven (time limit hit or early stop) |
infeasible | 200 | Yes | provably no solution exists — diagnostics block has details |
unknown | 200 | Yes | time-out without a solution and without proving infeasibility |
invalid | 400 | No | request schema ok but logically invalid (e.g. min > max) |
assignments
List of all placements. Only in the S3 object, not in the wire body. For status infeasible / unknown / invalid the block is missing entirely.
Parameter
| Name | Type | Status | Description |
|---|---|---|---|
entity | string | Yes | ID of the placed entity. |
placement | integer | Yes | Placement ordinal for this entity (1..max_placements). |
values | object | Yes | Concrete values per dimension for this placement. On interval dimensions the object additionally contains a key suffixed with _end for the end time. |
statistics
Performance and quality metrics. Always present in the wire body (also for offloaded responses).
Parameter
| Name | Type | Status | Description |
|---|---|---|---|
timing.model_build_seconds | float | Yes | Time to build variables and constraints before the actual solve. |
timing.solve_seconds | float | Yes | Pure solver time. |
timing.total_seconds | float | Yes | End-to-end inside the solver (model_build + solve). Billing basis. |
problem_size.dimensions | integer | Yes | Number of dimensions in the request. |
problem_size.entities | integer | Yes | Number of entities in the request. |
problem_size.constraints | integer | Yes | Number of constraints in the request. |
problem_size.variables | integer | Yes | Number of internal solver variables — indicator of problem size. |
problem_size.cells | integer | Yes | Number of cells (combinations of all dimension values). |
placements_per_entity | object | Yes | How often each entity ended up placed in the solution. |
total_penalty | integer | Yes | Sum of all soft-violation costs in the solution. 0 = no soft violations. |
best_bound | integer | Yes | Solver-proven lower bound on the penalty. When total_penalty == best_bound the solution is provably optimal. |
gap_percent | float | Yes | (total_penalty - best_bound) / total_penalty × 100. 0 = optimal. |
soft_constraint_violations | array | Yes | One entry per violated soft constraint with constraint_id, type, violation_count, penalty_total. |
diagnostics
Present only for status infeasible or unknown. Helps debug why the solver couldn't find a solution.
Parameter
| Name | Type | Status | Description |
|---|---|---|---|
message | string | Yes | Human-readable summary of the problem. |
capacity_conflicts | array | Optional | List of dimension values where demand exceeds capacity. Each entry: dimension, value, min_demand, available_slots, entities. |
conflicting_constraints | array | Optional | List of constraints that make the problem infeasible. Each entry: index, type, description. |