ommx 3.0.0-alpha.1

Open Mathematical prograMming eXchange (OMMX)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# Rust SDK Migration Guide

This document covers migration of the OMMX Rust SDK (`ommx` crate) across major versions.

- [v3 (Stage Pattern)]#rust-sdk-v3-stage-pattern-migration-guide — Constraint lifecycle stage parameterization

---

# Rust SDK v3 Stage Pattern Migration Guide

This section covers the migration to stage-parameterized constraints
landed in `3.0.0-alpha.1`.

## Overview

`Constraint` is now generic over a lifecycle stage, its `ConstraintID`
lives on the enclosing collection key rather than on the struct itself,
and metadata (`name`, `subscripts`, `parameters`, `description`,
`provenance`) lives in a Struct-of-Arrays store on the enclosing
collection — not on the per-constraint struct:

```rust,ignore
pub struct Constraint<S: Stage<Self> = Created> {
    pub equality: Equality,
    pub stage: S::Data,
}
```

Three lifecycle stages are defined:

| Type alias | Full type | Stage data |
|---|---|---|
| `Constraint` | `Constraint<Created>` | `CreatedData { function }` |
| `EvaluatedConstraint` | `Constraint<Evaluated>` | `EvaluatedData { evaluated_value, feasible, ... }` |
| `SampledConstraint` | `Constraint<stage::Sampled>` | `SampledData { evaluated_values, feasible, ... }` |

Removed constraints are managed at the collection level —
`ConstraintCollection` stores them as `(Constraint<Created>, RemovedReason)`
pairs. "Removed" is not itself a stage.

`DecisionVariable`, `IndicatorConstraint<S>`, `OneHotConstraint<S>`,
`Sos1Constraint<S>`, and `NamedFunction` got the same SoA treatment —
each lost its inline metadata fields, and the per-host metadata store
is queried through narrow per-collection accessors on `Instance` /
`ParametricInstance` (see [Metadata stores](#metadata-stores)).

## Breaking Changes

### 1. Constraint Field Access

Fields that were previously on the struct directly are now split
between common fields and stage-specific data. The `id` field is gone
entirely — look it up via the enclosing `BTreeMap` key.

**Common fields**:
```rust,ignore
// ❌ Before
constraint.id        // ConstraintID

// ✅ After — IDs live on collection keys
for (id, constraint) in instance.constraints() {
    // `id: &ConstraintID`, `constraint: &Constraint`
}

// ✅ Unchanged
constraint.equality  // Equality
```

**Metadata fields** (moved off the constraint struct entirely; query the
host's per-collection metadata store):
```rust,ignore
// ❌ Before (v2 — per-constraint inline)
constraint.name
constraint.subscripts
constraint.parameters
constraint.description

// ❌ Earlier v3 alpha (briefly: a `metadata` field on the struct) — also gone
constraint.metadata.name

// ✅ After — metadata lives in the SoA store on the enclosing collection
let store = instance.constraint_metadata();   // &ConstraintMetadataStore<ConstraintID>
store.name(id)         // Option<&str>
store.subscripts(id)   // &[i64]
store.parameters(id)   // &FnvHashMap<String, String>
store.description(id)  // Option<&str>
store.provenance(id)   // &[Provenance]

// One-shot owned reconstruction matching the pre-SoA struct
let metadata: ConstraintMetadata = store.collect_for(id);
```

The same shape applies to indicator / one-hot / sos1 constraints
(`indicator_constraint_metadata()`, …) and to decision variables
(`variable_metadata()` exposes a `VariableMetadataStore` without
`provenance`). For named functions the parallel accessor is
`named_function_metadata()` returning a `NamedFunctionMetadataStore`.

**Created stage** — function access:
```rust,ignore
// ❌ Before
constraint.function

// ✅ After (method)
constraint.function()       // &Function
constraint.function_mut()   // &mut Function

// ✅ After (direct field)
constraint.stage.function   // Function
```

**Evaluated stage** — evaluation result access:
```rust,ignore
// ❌ Before (getset methods)
*evaluated.evaluated_value()
*evaluated.feasible()
evaluated.dual_variable
evaluated.removed_reason()
evaluated.used_decision_variable_ids()

// ✅ After (direct stage field access)
evaluated.stage.evaluated_value
evaluated.stage.feasible
evaluated.stage.dual_variable
evaluated.stage.used_decision_variable_ids
```

`removed_reason` is no longer on evaluated/sampled constraints — it's managed by `EvaluatedCollection` / `SampledCollection` via `collection.removed_reasons()` and `collection.is_removed(&id)`.

**Sampled stage** — same pattern:
```rust,ignore
// ❌ Before
*sampled.evaluated_values()
sampled.feasible()
sampled.dual_variables

// ✅ After
sampled.stage.evaluated_values
sampled.stage.feasible
sampled.stage.dual_variables
```

### 2. Struct Literal Construction

**Constraint (Created)**:
```rust,ignore
// ❌ Before
Constraint {
    id: ConstraintID::from(1),
    function,
    equality: Equality::EqualToZero,
    name: None,
    subscripts: Vec::new(),
    parameters: FnvHashMap::default(),
    description: None,
}

// ✅ After — no `id` and no inline metadata
Constraint {
    equality: Equality::EqualToZero,
    stage: CreatedData { function },
}

// ✅ Factory methods no longer take an ID
Constraint::equal_to_zero(function)
Constraint::less_than_or_equal_to_zero(function)

// ✅ Insertion via the host's invariant-safe entry point — picks an
// unused id, drains the (optional) metadata into the SoA store,
// validates required_ids, returns the assigned id. `add_constraint`,
// `relax_constraint`, and `restore_constraint` all take `&mut self`,
// so `instance` must be a `mut` binding (or accessed via `&mut Instance`).
let id = instance.add_constraint(
    Constraint::equal_to_zero(function),
    ConstraintMetadata { name: Some("demand_balance".into()), ..Default::default() },
)?;

// `relax_constraint` / `restore_constraint` move id between active and
// removed; metadata stays in place. There is no `constraint_collection_mut()`
// — the raw map mutators on `ConstraintCollection<T>` are `pub(crate)`.
```

**Removed constraints** are no longer constructed as `Constraint<Removed>`. They are stored as `(Constraint<Created>, RemovedReason)` tuples in `ConstraintCollection`:
```rust,ignore
// ❌ Before (v2)
let removed = RemovedConstraint {
    constraint: inner_constraint,
    removed_reason: "reason".to_string(),
    removed_reason_parameters: Default::default(),
};

// ✅ After — use Instance::relax_constraint() or store tuples directly
instance.relax_constraint(id, "reason".to_string(), [])?;

// Or if constructing directly:
let removed: (Constraint, RemovedReason) = (
    constraint,
    RemovedReason {
        reason: "reason".to_string(),
        parameters: Default::default(),
    },
);
```

**EvaluatedConstraint**:
```rust,ignore
// ❌ Before
EvaluatedConstraint {
    id, equality, metadata,
    evaluated_value,
    feasible,
    dual_variable: None,
    used_decision_variable_ids,
    removed_reason: None,
    removed_reason_parameters: FnvHashMap::default(),
}

// ✅ After — no `id`, no inline metadata; insert with the key when
// storing. Metadata for the id rides on the parent
// `EvaluatedCollection<T>::metadata` SoA store.
Constraint {
    equality,
    stage: EvaluatedData {
        evaluated_value,
        feasible,
        dual_variable: None,
        used_decision_variable_ids,
    },
}
```

### 3. RemovedConstraint Removed

`RemovedConstraint` type alias no longer exists. Removed constraints are stored as `(Constraint<Created>, RemovedReason)` tuples in `ConstraintCollection`.

```rust,ignore
// ❌ Before (v2)
removed.constraint.id
removed.constraint.equality
removed.constraint.function
removed.removed_reason              // String
removed.removed_reason_parameters   // FnvHashMap<String, String>

// ✅ After — access via the tuple; the ID comes from the map key
let (constraint, reason) = collection.removed().get(&id).unwrap();
// id is the BTreeMap key you looked it up by
constraint.equality
constraint.function()
reason.reason
reason.parameters
```

### 4. RemovedReason Struct

`removed_reason: String` + `removed_reason_parameters: FnvHashMap<String, String>` are consolidated into a single struct:

```rust,ignore
pub struct RemovedReason {
    pub reason: String,
    pub parameters: FnvHashMap<String, String>,
}
```

`RemovedReason` is stored at the collection level, not on individual constraints:
- `ConstraintCollection.removed()``&BTreeMap<ID, (T::Created, RemovedReason)>`
- `EvaluatedCollection.removed_reasons()``&BTreeMap<ID, RemovedReason>`
- `SampledCollection.removed_reasons()``&BTreeMap<ID, RemovedReason>`

### 5. Instance Fields

`Instance.constraints` and `Instance.removed_constraints` fields are replaced by `constraint_collection: ConstraintCollection<Constraint>`.

Accessor methods are preserved for backward compatibility:
```rust,ignore
// These still work
instance.constraints()           // &BTreeMap<ConstraintID, Constraint>
instance.removed_constraints()   // &BTreeMap<ConstraintID, (Constraint, RemovedReason)>

// New: access the full collection
instance.constraint_collection() // &ConstraintCollection<Constraint>
```

For mutable access, downstream code goes through invariant-safe
`Instance` / `ParametricInstance` methods (`add_constraint`,
`insert_constraint`, `relax_constraint`, `restore_constraint`, …) —
these validate that every `id` referenced by the constraint exists in
`decision_variables` and keep the active / removed maps disjoint. The
raw `active_mut()` / `removed_mut()` mutators on
`ConstraintCollection<T>` are `pub(crate)` and not callable from
outside the crate.

### 6. getset Removal

`EvaluatedConstraint` and `SampledConstraint` no longer use the `getset` crate. All fields are accessed directly via `self.equality` and `self.stage.*`. (`self.id` and `self.metadata` no longer exist on the struct — see [Metadata stores](#metadata-stores) and the constraint-field-access section above.)

Methods like `.id()`, `.equality()`, `.evaluated_value()`, `.feasible()` are **removed**. Use field access instead.

### 7. Error Surface Call-Site Rewrites

See the [release note](crate::doc::release_note) for the
rationale and the [error handling tutorial](crate::doc::tutorial::error_handling)
for the `ommx::Result` / signal-type / fail-site-macro story. This
section only lists the mechanical call-site rewrites you need to apply
when upgrading a crate that was on v2.

**Deleted error enums.** The types below no longer exist. Match arms
that inspected their variants should switch to string inspection (if
you really cared) or just propagate via `?`:

- `ommx::InstanceError`
- `ommx::MpsParseError`, `ommx::MpsWriteError`
- `ommx::StateValidationError`, `ommx::LogEncodingError`
- `ommx::UnknownSampleIDError` — replaced by `Option<T>` on key-lookup methods
- `ommx::ParseErrorReason` — the variant enum inside the old `ommx::QplibParseError`

```rust,ignore
// ❌ Before (v2)
match decode(bytes) {
    Err(InstanceError::DuplicateConstraintID(id)) => { ... }
    Err(InstanceError::UndefinedVariable(v)) => { ... }
    Err(e) => return Err(e),
    Ok(x) => x,
}

// ✅ After (v3) — either propagate, or inspect the rendered message
let instance = decode(bytes)?;
```

**Moved / renamed error types:**

- `ommx::QplibParseError``ommx::qplib::QplibParseError`. The type is
  slimmer (1-based `line_num` + rendered `message`, no variant enum),
  and no longer re-exported at the crate root.

**Key lookups now return `Option<T>`:**

```rust,ignore
// ❌ Before — typed Err when the key was missing
let solution: Solution = sample_set
    .get(id)
    .map_err(|UnknownSampleIDError { .. }| /* handle */)?;

// ✅ After — Option, lifted at the boundary if your caller wants Result
let solution: Solution = sample_set
    .get(id)
    .ok_or_else(|| ommx::error!("unknown sample id {id:?}"))?;
```

**Signal-type recovery is unchanged in syntax** — it just now flows
through `ommx::Error` instead of a bespoke enum:

```rust,ignore
match instance.propagate(&state, atol) {
    Err(e) if e.is::<ommx::InfeasibleDetected>() => { /* handle */ }
    Err(e) => return Err(e),
    Ok(outcome) => { /* ... */ }
}
```

## New Types

### ConstraintType Trait

A type family mapping lifecycle stages to concrete types (HKT defunctionalization):

```rust,ignore
pub trait ConstraintType {
    type ID: Clone + Copy + Ord + Hash + Debug;
    type Created: Evaluate<Output = Self::Evaluated, SampledOutput = Self::Sampled>
        + Clone + Debug + PartialEq;
    type Evaluated: EvaluatedConstraintBehavior<ID = Self::ID>;
    type Sampled: SampledConstraintBehavior<ID = Self::ID, Evaluated = Self::Evaluated>;
}

// Regular constraints
impl ConstraintType for Constraint {
    type ID = ConstraintID;
    // ...
}

// Indicator constraints
impl ConstraintType for IndicatorConstraint {
    type ID = IndicatorConstraintID;
    // ...
}
```

### Behavior Traits

Two traits define common behavior for evaluated and sampled constraints:

```rust,ignore
pub trait EvaluatedConstraintBehavior {
    type ID;
    fn is_feasible(&self) -> bool;
}

pub trait SampledConstraintBehavior {
    type ID;
    type Evaluated;
    fn is_feasible_for(&self, sample_id: SampleID) -> Option<bool>;
    fn get(&self, sample_id: SampleID) -> Option<Self::Evaluated>;
}
```

Neither trait exposes the constraint's ID — it lives on the enclosing
`BTreeMap` key. `is_removed()` is similarly absent from the traits;
use `EvaluatedCollection::is_removed(&id)` or
`SampledCollection::is_removed(&id)` instead.

### ConstraintCollection

Generic collection of active + removed constraints, plus the SoA
metadata store for the kind. Also implements `Evaluate`:

```rust,ignore
pub struct ConstraintCollection<T: ConstraintType> {
    active: BTreeMap<T::ID, T::Created>,
    removed: BTreeMap<T::ID, (T::Created, RemovedReason)>,
    metadata: ConstraintMetadataStore<T::ID>,
}

// Methods (public)
collection.active()                    // &BTreeMap<T::ID, T::Created>
collection.removed()                   // &BTreeMap<T::ID, (T::Created, RemovedReason)>
collection.metadata()                  // &ConstraintMetadataStore<T::ID>
collection.into_parts()                // (active, removed, metadata)
// Mutation goes through Instance / ParametricInstance methods so
// invariants (active/removed disjointness, variable-id validity)
// are enforced; the raw `active_mut` / `removed_mut` / `insert_with`
// primitives on this type are `pub(crate)`.

// Evaluate trait impl
collection.evaluate(state, atol)           // EvaluatedCollection<T>
collection.evaluate_samples(samples, atol) // SampledCollection<T>
collection.partial_evaluate(state, atol)   // only active constraints
collection.required_ids()                  // VariableIDSet
```

Removed constraints are just `Created` constraints paired with a `RemovedReason`. The `Removed` stage type no longer exists.

### EvaluatedCollection / SampledCollection

Generic wrappers for evaluation results, used in `Solution` and `SampleSet`. Each carries the same `ConstraintMetadataStore<T::ID>` as the source `ConstraintCollection<T>` so per-id metadata is available at every stage:

```rust,ignore
pub struct EvaluatedCollection<T: ConstraintType> {
    constraints: BTreeMap<T::ID, T::Evaluated>,
    removed_reasons: BTreeMap<T::ID, RemovedReason>,
    metadata: ConstraintMetadataStore<T::ID>,
}

pub struct SampledCollection<T: ConstraintType> {
    constraints: BTreeMap<T::ID, T::Sampled>,
    removed_reasons: BTreeMap<T::ID, RemovedReason>,
    metadata: ConstraintMetadataStore<T::ID>,
}

// Both Deref to BTreeMap<T::ID, T::Evaluated/Sampled> for backward-compatible access
// and provide feasibility / removal / metadata accessors:
collection.is_feasible()               // all constraints feasible
collection.is_feasible_relaxed()       // all non-removed constraints feasible
collection.is_removed(&id)             // check if a constraint was removed
collection.removed_reasons()           // &BTreeMap<T::ID, RemovedReason>
collection.metadata()                  // &ConstraintMetadataStore<T::ID>
```

### Metadata stores

Per-collection Struct-of-Arrays metadata stores replace the inline
metadata fields that used to live on every `Constraint` /
`DecisionVariable` / `NamedFunction`. Three families:

```rust,ignore
pub struct ConstraintMetadataStore<ID> { /* name / subscripts / parameters / description / provenance */ }
pub struct VariableMetadataStore       { /* same, no provenance */ }
pub struct NamedFunctionMetadataStore  { /* same, no provenance */ }
```

Per-host accessors on `Instance` and `ParametricInstance` give direct
read / write access to every store:

```rust,ignore
instance.constraint_metadata()              // &ConstraintMetadataStore<ConstraintID>
instance.constraint_metadata_mut()          // &mut …
instance.indicator_constraint_metadata()    // &ConstraintMetadataStore<IndicatorConstraintID>
instance.indicator_constraint_metadata_mut()
instance.one_hot_constraint_metadata() / _mut()
instance.sos1_constraint_metadata()    / _mut()
instance.variable_metadata()           / _mut()        // &VariableMetadataStore
instance.named_function_metadata()     / _mut()        // &NamedFunctionMetadataStore
```

`Solution` and `SampleSet` expose the variable / named-function stores
the same way (`solution.variable_metadata()`,
`solution.named_function_metadata()`, same on `SampleSet`), but
constraint metadata is reached through the evaluated / sampled
collection getter then `.metadata()` on the collection — there are no
flattened `solution.constraint_metadata()` shortcuts at the host level:

```rust,ignore
solution.evaluated_constraints().metadata()              // &ConstraintMetadataStore<ConstraintID>
solution.evaluated_indicator_constraints().metadata()    // … <IndicatorConstraintID>
solution.evaluated_one_hot_constraints().metadata()
solution.evaluated_sos1_constraints().metadata()

sample_set.constraints().metadata()                      // &ConstraintMetadataStore<ConstraintID>
sample_set.indicator_constraints().metadata()
// etc.
```

Store API:

```rust,ignore
impl<ID> ConstraintMetadataStore<ID> {
    // Per-field borrowing reads. EMPTY_* sentinels cover the absent case
    // so the underlying Option<…> storage doesn't leak through.
    pub fn name(&self, id: ID)        -> Option<&str>;
    pub fn subscripts(&self, id: ID)  -> &[i64];
    pub fn parameters(&self, id: ID)  -> &FnvHashMap<String, String>;
    pub fn description(&self, id: ID) -> Option<&str>;
    pub fn provenance(&self, id: ID)  -> &[Provenance];

    // One-shot owned reconstruction matching the I/O struct.
    pub fn collect_for(&self, id: ID) -> ConstraintMetadata;

    // Setters (write-through to the SoA store).
    pub fn set_name(&mut self, id: ID, name: impl Into<String>);
    pub fn set_subscripts(&mut self, id: ID, s: impl Into<Vec<i64>>);
    pub fn push_subscript(&mut self, id: ID, value: i64);
    pub fn set_parameter(&mut self, id: ID, key: impl Into<String>, value: impl Into<String>);
    pub fn set_parameters(&mut self, id: ID, params: FnvHashMap<String, String>);
    pub fn set_description(&mut self, id: ID, desc: impl Into<String>);
    pub fn push_provenance(&mut self, id: ID, p: Provenance);
    pub fn set_provenance(&mut self, id: ID, p: Vec<Provenance>);

    // Bulk owned exchange with the I/O struct.
    pub fn insert(&mut self, id: ID, metadata: ConstraintMetadata);
    pub fn remove(&mut self, id: ID) -> ConstraintMetadata;
}
```

`VariableMetadataStore` and `NamedFunctionMetadataStore` mirror the
shape above with the provenance fields omitted (`provenance(id)`,
`push_provenance`, `set_provenance`). `VariableMetadataStore` keeps the
subscript append helpers (`push_subscript`, `extend_subscripts`);
`NamedFunctionMetadataStore` does not — extend a named function's
subscripts via `set_subscripts(id, new_vec)` instead.

### ConstraintMetadata

Owned struct used as the I/O type for metadata (insertion via
`add_constraint(c, metadata)`, owned reads via `store.collect_for(id)`,
modeling-chain staging on the Python `Constraint` snapshot wrapper).
Same shape as the pre-SoA struct:

```rust,ignore
pub struct ConstraintMetadata {
    pub name: Option<String>,
    pub subscripts: Vec<i64>,
    pub parameters: FnvHashMap<String, String>,
    pub description: Option<String>,
    /// Chain of transformations that produced this constraint.
    /// Empty for directly-authored constraints; populated when e.g. an
    /// IndicatorConstraint is promoted to a regular Constraint.
    pub provenance: Vec<Provenance>,
}
```

## Migration Checklist

- [ ] Remove `constraint.id` reads — look up the ID via the enclosing `BTreeMap<ConstraintID, _>` key instead
- [ ] Update `Constraint::equal_to_zero(id, function)` / `Constraint::less_than_or_equal_to_zero(id, function)` → drop the ID argument (`Constraint::equal_to_zero(function)`), insert with the key
- [ ] Update `constraint.function``constraint.function()` or `constraint.stage.function`
- [ ] Update `constraint.name` reads — metadata is no longer on the constraint struct. Query the host's SoA store: `instance.constraint_metadata().name(id)` (and `subscripts`, `parameters`, `description`, `provenance`); use `collect_for(id) -> ConstraintMetadata` for an owned snapshot.
- [ ] Update `evaluated.evaluated_value()``evaluated.stage.evaluated_value` (and other getset methods)
- [ ] Update `RemovedConstraint` construction → `(Constraint, RemovedReason)` tuple
- [ ] Update `removed.constraint.xxx``removed.0.xxx` (tuple access)
- [ ] Update `removed_reason` / `removed_reason_parameters``RemovedReason { reason, parameters }`
- [ ] Update `evaluated.removed_reason()``collection.removed_reasons().get(&id)`
- [ ] Update struct literals to use `stage: CreatedData { ... }` / `EvaluatedData { ... }` / etc.
- [ ] Update `self.constraints` / `self.removed_constraints``self.constraint_collection.active()` / `.removed()`
- [ ] Remove any `getset` usage for constraint types
- [ ] Update any `InstanceError` / `MpsParseError` / `QplibParseError` / `StateValidationError` / `LogEncodingError` / `UnknownSampleIDError` matches → inspect `err.to_string()` or use `err.downcast_ref::<T>()` for signal types
- [ ] Replace `Result<T, UnknownSampleIDError>` key-lookup methods with `Option<T>` on the call site