# Problem modeling
Convert the user's problem into SolverForge's four building blocks, confirm it
back, then build it.
## The intake template
After the user answers the Step 0 questions, write this back to them before
scaffolding:
```
Solution: <Name> (<ScoreType>)
Facts: <Plural> — <why immutable>
Entities: <Plural> — <what gets a value assigned>
Variables: <Entity>.<field> [scalar|list] over <source>
Hard: <id> — <what must hold>
Soft: <id> — <what should be optimized>
Do not scaffold until the user agrees with this restatement. It prevents the
common failure of building the wrong model and only discovering it at solve time.
## The four blocks
| Problem fact | What is fixed input that the solver must not change? | `generate fact` → `#[problem_fact]`, `Vec<T>` collection |
| Planning entity | What am I deciding *about*? Each instance gets assigned. | `generate entity` → `#[planning_entity]`, `Vec<T>` collection |
| Planning variable | What is the decision itself? | `generate variable` → `scalar` or `list` on an entity |
| Constraint | What makes a plan valid or good? | `generate constraint` → one module, hard or soft |
| Score | How are hard/soft violations ranked? | `generate solution --score` / `generate score` |
### Facts vs entities
- If the solver may change it, it is an entity.
- If it is an input the solver reads but must not mutate, it is a fact.
- Counts (capacity, demand, priority, duration) belong on facts/entities as
fields; they are **not** planning variables.
- A "resource", "employee", "vehicle", "machine", "room" is usually a fact; the
thing being scheduled/assigned/routed is usually an entity.
### Scalar vs list
- **Scalar** — one decision value per entity. Two sources:
- `--range <fact_plural>`: the value is the **index** into that fact
collection. Add `--allows-unassigned` if an entity may go unassigned.
- `--countable-range <from..to>`: the value is an integer in `[from, to)`.
Use for time slots, positions, quantities.
- **List** — an ordered sequence owned by the entity (routes, tours, line
sequences, precedence-ordered jobs). Uses `--elements <fact_plural>`. Use the
`--domain cvrp` profile for vehicle-routing, or the explicit list-metadata
flags for custom sequences.
Never model an ordered sequence as a scalar predecessor field. That is the most
common modeling error and it is explicitly not supported.
## Model the rule, don't precompute it
The most damaging shortcut is to decide legality in Rust ahead of the solver.
Do not build entity-owned feasibility verdicts (`task.feasible: Vec<bool>`,
per-slot `allowed: Vec<bool>`, minute-offset tables) and then score a penalty
against that flag. It compiles, it scores, and it hides the rule from score
analysis while letting the solver return plans the matrix was supposed to
forbid.
- State each rule once, over the domain objects, inside a constraint
(`constraint-patterns.md`). The constraint reads the entity's assigned value
and the fact that owns the relevant data.
- Keep derived data on the object that owns it as **input**: a fact's
availability calendar, a slot's start/end, or a prepared travel-time matrix.
What is forbidden is a per-entity verdict (this task may not use this slot),
not the factual data a constraint reads and scores.
- Never add a greedy initializer, construction heuristic, or post-solve
sanitizer in application code. Candidates are restricted through the
variable's candidate/value-range metadata; search policy lives in
`solver.toml`.
- Explain results with `analyze()` / `evaluate_detailed`, not with a second copy
of the rule written as reporting predicates.
## Modeling time and windows
- **Duration belongs to the entity; the window belongs to a fact.** If a task
has a duration and a timeslot has a start/end, add a hard constraint that the
duration fits the window, or size the slot facts to the entity. Do not
precompute "end minute" offsets onto the entity.
- **"Now" is a horizon boundary, not a penalty.** If the plan must not be
scheduled in the past, build the slot/visit facts so periods before `now` are
never generated. A past slot left in the candidate range will be chosen unless
a constraint catches it, and a hard penalty can lose to a local optimum.
- **Existing busy intervals are facts, not entities.** Model a pre-existing
booking/absence as fact data that a conflict constraint joins against, so
overlap is computed from the same stream logic as new assignments.
- **Overlaps use project-then-self-join**, not dense-slot equality. See the
recipe and worked `uc-lessons` links in `constraint-patterns.md`.
- **Unassigned is a separate concern.** Use a hard `.unassigned()` when
assignment is mandatory, or a medium penalty when "schedule as much as
possible" is a preference. Do not duplicate the assignment penalty inside
overlap/conflict rules.
## Score type and hardness
- Default `HardSoftScore`: hard = validity, soft = quality. Almost always this.
- `HardMediumSoftScore` when the user has a middle tier.
- `HardSoftDecimalScore` for fractional weights.
- `SoftScore` for pure optimization; it has no hard level, so every generated
constraint must be `--soft` (the CLI rejects hard constraints on `SoftScore`).
- `BendableScore<N, M>` for custom counts of hard/soft levels.
- Soft weights let you express priority: a violation that costs 10 is ten times
as bad as one that costs 1. Use explicit `one_hard()` / `one_soft()` only when
all violations are equal.
## Field types
`--field name:Type` accepts any Rust type the struct can hold. Practical menu:
| `String` | labels, ids, categories | `"<stem>-<field>-<idx>"` |
| `i32`, `i64` | counts, weights, priorities | `(idx % 7) - 2`, `(idx % 11) - 5` |
| `f32`, `f64` | coordinates, durations, loads | `(idx % 9) * 1.25`, `(idx % 13) * 1.25` |
| `bool` | flags | `idx % 2 == 0` |
| `usize` | external indexes, slots | `idx + 1` |
| `Option<T>` | optional data | `None` |
| `Vec<T>` | inline lists | `vec![]` |
These sample formulas are fixed by the CLI. If a constraint depends on field
values having a particular relationship (e.g. demand exceeding capacity), the
defaults may make the constraint trivially satisfied or trivially violated. Pick
field types/names with the sample formulas in mind, or load real data (see
`gotchas.md`).
## Worked examples
### Scheduling / assignment
```
Solution: Schedule (HardSoftScore)
Facts: employees — fixed workforce
Entities: shifts — each shift needs an employee
Variables: Shift.employee_idx scalar over employees, --allows-unassigned
Hard: no_overlap — one employee cannot cover two shifts at the same time
Soft: prefer_senior — reward assigning a shift's preferred employee
Output: web
```
### Vehicle routing
```
Solution: Plan (HardSoftScore)
Facts: visits, vehicles
Entities: routes — each route is one vehicle's ordered tour
Variables: Route.stops list over visits, --domain cvrp
Hard: capacity — vehicle capacity must cover its route demand
Soft: distance — minimize total travel
Output: api
```
List variables model ordered sequences in general — routes, ordered
assignments, job sequencing, precedence lists. Most never touch geography; use
`references/routing-and-maps.md` only when the ordering cost is real road
travel.
### Time-slot placement
```
Solution: Plan (HardSoftScore)
Facts: rooms
Entities: meetings
Variables: Meeting.room_idx scalar over rooms --allows-unassigned
Meeting.start_slot scalar --countable-range 0..24
Hard: room_no_overlap — no two meetings share a room and slot
Soft: prefer_morning — reward early slots
Output: web
```
This dense countable-range form is fine only when every slot is legal and
interchangeable. When slots have real times, per-teacher/room availability, or a
"now" horizon, model slots as **facts** that own `day`/`start`/`end` and let the
hard constraints read them, generating only legal periods. See the
`uc-lessons`-derived time/window/overlap recipe in `constraint-patterns.md`.
## Constraint inventory checklist
For each constraint, capture: exact `snake_case` id, hard/soft, the entities and
facts it reads, the trigger condition, and the weight. A constraint with no
condition is not a constraint. Feed these straight into `generate constraint`
plus a rewritten body (see `constraint-patterns.md`).