solverforge 0.2.5__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {solverforge-0.2.5 → solverforge-0.2.6}/Cargo.lock +5 -5
  2. {solverforge-0.2.5 → solverforge-0.2.6}/Cargo.toml +1 -1
  3. {solverforge-0.2.5 → solverforge-0.2.6}/PKG-INFO +1 -1
  4. {solverforge-0.2.5 → solverforge-0.2.6}/pyproject.toml +1 -1
  5. {solverforge-0.2.5 → solverforge-0.2.6}/python/solverforge/__init__.py +4 -0
  6. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/python/solverforge/__init__.py +4 -0
  7. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/collectors.rs +89 -0
  8. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/lambda_analyzer.rs +97 -1
  9. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/score.rs +687 -0
  10. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/Cargo.toml +1 -1
  11. {solverforge-0.2.5 → solverforge-0.2.6}/README.md +0 -0
  12. {solverforge-0.2.5 → solverforge-0.2.6}/python/solverforge/solver/__init__.py +0 -0
  13. {solverforge-0.2.5 → solverforge-0.2.6}/python/solverforge/solver/config.py +0 -0
  14. {solverforge-0.2.5 → solverforge-0.2.6}/python/solverforge/solver/domain.py +0 -0
  15. {solverforge-0.2.5 → solverforge-0.2.6}/python/solverforge/solver/score.py +0 -0
  16. {solverforge-0.2.5 → solverforge-0.2.6}/python/solverforge/test/__init__.py +0 -0
  17. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/Cargo.toml +0 -0
  18. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/README.md +0 -0
  19. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/analysis/explanation.rs +0 -0
  20. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/analysis/manager.rs +0 -0
  21. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/analysis/mod.rs +0 -0
  22. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/bridge.rs +0 -0
  23. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/constraints/collectors.rs +0 -0
  24. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/constraints/constraint.rs +0 -0
  25. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/constraints/joiners.rs +0 -0
  26. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/constraints/mod.rs +0 -0
  27. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/constraints/stream.rs +0 -0
  28. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/domain/annotations.rs +0 -0
  29. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/domain/class.rs +0 -0
  30. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/domain/constraint_config.rs +0 -0
  31. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/domain/mod.rs +0 -0
  32. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/domain/model.rs +0 -0
  33. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/domain/shadow.rs +0 -0
  34. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/error.rs +0 -0
  35. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/handles.rs +0 -0
  36. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/lib.rs +0 -0
  37. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/score/bendable.rs +0 -0
  38. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/score/hard_soft.rs +0 -0
  39. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/score/hard_soft_decimal.rs +0 -0
  40. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/score/mod.rs +0 -0
  41. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/score/simple.rs +0 -0
  42. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/score/simple_decimal.rs +0 -0
  43. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/builder.rs +0 -0
  44. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/client.rs +0 -0
  45. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/config.rs +0 -0
  46. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/environment.rs +0 -0
  47. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/factory.rs +0 -0
  48. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/mod.rs +0 -0
  49. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/request.rs +0 -0
  50. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/response.rs +0 -0
  51. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/solver/termination.rs +0 -0
  52. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/traits.rs +0 -0
  53. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/value.rs +0 -0
  54. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/wasm/expr_builder.rs +0 -0
  55. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/wasm/expression.rs +0 -0
  56. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/wasm/generator.rs +0 -0
  57. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/wasm/host_functions.rs +0 -0
  58. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/wasm/memory.rs +0 -0
  59. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-core/src/wasm/mod.rs +0 -0
  60. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/Cargo.toml +0 -0
  61. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/README.md +0 -0
  62. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/python/solverforge/solver/__init__.py +0 -0
  63. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/python/solverforge/solver/config.py +0 -0
  64. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/python/solverforge/solver/domain.py +0 -0
  65. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/python/solverforge/solver/score.py +0 -0
  66. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/python/solverforge/test/__init__.py +0 -0
  67. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/analysis.rs +0 -0
  68. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/annotations.rs +0 -0
  69. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/bridge.rs +0 -0
  70. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/decorators.rs +0 -0
  71. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/joiners.rs +0 -0
  72. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/lib.rs +0 -0
  73. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/service.rs +0 -0
  74. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/solver.rs +0 -0
  75. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-python/src/stream.rs +0 -0
  76. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/README.md +0 -0
  77. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/src/config.rs +0 -0
  78. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/src/error.rs +0 -0
  79. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/src/jar.rs +0 -0
  80. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/src/lib.rs +0 -0
  81. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/src/service.rs +0 -0
  82. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/src/util.rs +0 -0
  83. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/tests/employee_scheduling_integration.rs +0 -0
  84. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/tests/integration.rs +0 -0
  85. {solverforge-0.2.5 → solverforge-0.2.6}/solverforge-service/tests/solve_integration.rs +0 -0
@@ -1507,7 +1507,7 @@ dependencies = [
1507
1507
 
1508
1508
  [[package]]
1509
1509
  name = "solverforge"
1510
- version = "0.2.5"
1510
+ version = "0.2.6"
1511
1511
  dependencies = [
1512
1512
  "solverforge-core",
1513
1513
  "solverforge-derive",
@@ -1516,7 +1516,7 @@ dependencies = [
1516
1516
 
1517
1517
  [[package]]
1518
1518
  name = "solverforge-core"
1519
- version = "0.2.5"
1519
+ version = "0.2.6"
1520
1520
  dependencies = [
1521
1521
  "base64",
1522
1522
  "indexmap",
@@ -1532,7 +1532,7 @@ dependencies = [
1532
1532
 
1533
1533
  [[package]]
1534
1534
  name = "solverforge-derive"
1535
- version = "0.2.5"
1535
+ version = "0.2.6"
1536
1536
  dependencies = [
1537
1537
  "proc-macro2",
1538
1538
  "quote",
@@ -1544,7 +1544,7 @@ dependencies = [
1544
1544
 
1545
1545
  [[package]]
1546
1546
  name = "solverforge-python"
1547
- version = "0.2.5"
1547
+ version = "0.2.6"
1548
1548
  dependencies = [
1549
1549
  "log",
1550
1550
  "pyo3",
@@ -1559,7 +1559,7 @@ dependencies = [
1559
1559
 
1560
1560
  [[package]]
1561
1561
  name = "solverforge-service"
1562
- version = "0.2.5"
1562
+ version = "0.2.6"
1563
1563
  dependencies = [
1564
1564
  "base64",
1565
1565
  "chrono",
@@ -3,7 +3,7 @@ members = ["solverforge-core", "solverforge-python", "solverforge-service"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.5"
6
+ version = "0.2.6"
7
7
  edition = "2021"
8
8
  license = "Apache-2.0"
9
9
  repository = "https://github.com/solverforge/solverforge"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solverforge
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "solverforge"
7
- version = "0.2.5"
7
+ version = "0.2.6"
8
8
  description = "Python bindings for SolverForge constraint solver"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -33,6 +33,8 @@ from solverforge._solverforge import (
33
33
  HardMediumSoftScore,
34
34
  HardSoftDecimalScore,
35
35
  HardMediumSoftDecimalScore,
36
+ BendableScore,
37
+ BendableDecimalScore,
36
38
  # Decorators
37
39
  planning_entity,
38
40
  planning_solution,
@@ -105,6 +107,8 @@ __all__ = [
105
107
  "HardMediumSoftScore",
106
108
  "HardSoftDecimalScore",
107
109
  "HardMediumSoftDecimalScore",
110
+ "BendableScore",
111
+ "BendableDecimalScore",
108
112
  # Decorators
109
113
  "planning_entity",
110
114
  "planning_solution",
@@ -33,6 +33,8 @@ from solverforge._solverforge import (
33
33
  HardMediumSoftScore,
34
34
  HardSoftDecimalScore,
35
35
  HardMediumSoftDecimalScore,
36
+ BendableScore,
37
+ BendableDecimalScore,
36
38
  # Decorators
37
39
  planning_entity,
38
40
  planning_solution,
@@ -105,6 +107,8 @@ __all__ = [
105
107
  "HardMediumSoftScore",
106
108
  "HardSoftDecimalScore",
107
109
  "HardMediumSoftDecimalScore",
110
+ "BendableScore",
111
+ "BendableDecimalScore",
108
112
  # Decorators
109
113
  "planning_entity",
110
114
  "planning_solution",
@@ -244,6 +244,57 @@ impl PyConstraintCollectors {
244
244
  })
245
245
  }
246
246
 
247
+ /// Compose multiple collectors into a single result.
248
+ ///
249
+ /// # Arguments
250
+ /// * `collectors` - List of collectors to compose
251
+ /// * `combiner` - Lambda that combines the results into a single value
252
+ ///
253
+ /// # Example
254
+ /// ```python
255
+ /// ConstraintCollectors.compose(
256
+ /// [ConstraintCollectors.count(), ConstraintCollectors.sum(lambda x: x.value)],
257
+ /// lambda count, total: total / count if count > 0 else 0
258
+ /// )
259
+ /// ```
260
+ #[staticmethod]
261
+ fn compose(
262
+ py: Python<'_>,
263
+ collectors: Vec<PyCollector>,
264
+ combiner: Py<PyAny>,
265
+ ) -> PyResult<PyCollector> {
266
+ let rust_collectors: Vec<Collector> = collectors.iter().map(|c| c.to_rust()).collect();
267
+ let combiner_info = LambdaInfo::new(py, combiner, "compose_combiner")?;
268
+ Ok(PyCollector {
269
+ inner: Collector::compose(rust_collectors, combiner_info.to_wasm_function()),
270
+ })
271
+ }
272
+
273
+ /// Apply a collector conditionally based on a predicate.
274
+ ///
275
+ /// # Arguments
276
+ /// * `predicate` - Lambda that returns true if the item should be collected
277
+ /// * `collector` - The collector to apply when predicate is true
278
+ ///
279
+ /// # Example
280
+ /// ```python
281
+ /// ConstraintCollectors.conditionally(
282
+ /// lambda shift: shift.is_overtime,
283
+ /// ConstraintCollectors.count()
284
+ /// )
285
+ /// ```
286
+ #[staticmethod]
287
+ fn conditionally(
288
+ py: Python<'_>,
289
+ predicate: Py<PyAny>,
290
+ collector: PyCollector,
291
+ ) -> PyResult<PyCollector> {
292
+ let pred_info = LambdaInfo::new(py, predicate, "conditionally_pred")?;
293
+ Ok(PyCollector {
294
+ inner: Collector::conditionally(pred_info.to_wasm_function(), collector.to_rust()),
295
+ })
296
+ }
297
+
247
298
  fn __repr__(&self) -> &'static str {
248
299
  "ConstraintCollectors()"
249
300
  }
@@ -439,4 +490,42 @@ mod tests {
439
490
  let repr = collector.__repr__();
440
491
  assert!(repr.contains("Count"));
441
492
  }
493
+
494
+ #[test]
495
+ fn test_compose_collector() {
496
+ init_python();
497
+ Python::with_gil(|py| {
498
+ let locals = PyDict::new(py);
499
+ py.run(c"combiner = lambda a, b: a + b", None, Some(&locals))
500
+ .unwrap();
501
+ let combiner = locals.get_item("combiner").unwrap().unwrap();
502
+
503
+ let collectors = vec![
504
+ PyConstraintCollectors::count(),
505
+ PyConstraintCollectors::count_distinct(),
506
+ ];
507
+
508
+ let collector =
509
+ PyConstraintCollectors::compose(py, collectors, combiner.unbind()).unwrap();
510
+ let rust_collector = collector.to_rust();
511
+ assert!(matches!(rust_collector, Collector::Compose { .. }));
512
+ });
513
+ }
514
+
515
+ #[test]
516
+ fn test_conditionally_collector() {
517
+ init_python();
518
+ Python::with_gil(|py| {
519
+ let locals = PyDict::new(py);
520
+ py.run(c"pred = lambda x: x.active", None, Some(&locals))
521
+ .unwrap();
522
+ let pred = locals.get_item("pred").unwrap().unwrap();
523
+
524
+ let inner = PyConstraintCollectors::count();
525
+ let collector =
526
+ PyConstraintCollectors::conditionally(py, pred.unbind(), inner).unwrap();
527
+ let rust_collector = collector.to_rust();
528
+ assert!(matches!(rust_collector, Collector::Conditionally { .. }));
529
+ });
530
+ }
442
531
  }
@@ -172,7 +172,7 @@ fn analyze_lambda_bytecode(py: Python<'_>, lambda_info: &LambdaInfo) -> PyResult
172
172
  "RESUME" | "PRECALL" | "PUSH_NULL" | "COPY_FREE_VARS" | "CACHE" => {
173
173
  // Skip these opcodes
174
174
  }
175
- "LOAD_FAST" | "LOAD_FAST_CHECK" => {
175
+ "LOAD_FAST" | "LOAD_FAST_CHECK" | "LOAD_FAST_AND_CLEAR" => {
176
176
  // Load a local variable (parameter) - argval is the variable name
177
177
  // We need to find the parameter index
178
178
  let var_name: String = argval.extract()?;
@@ -180,6 +180,29 @@ fn analyze_lambda_bytecode(py: Python<'_>, lambda_info: &LambdaInfo) -> PyResult
180
180
  let varnames: Vec<String> = code.getattr("co_varnames")?.extract()?;
181
181
  if let Some(idx) = varnames.iter().position(|n| n == &var_name) {
182
182
  stack.push(BytecodeValue::Param(idx as u32));
183
+ } else {
184
+ // Variable not in varnames - could be a closure variable
185
+ return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
186
+ "Cannot analyze lambda: unknown variable '{}'. Lambda parameters must be used directly.",
187
+ var_name
188
+ )));
189
+ }
190
+ }
191
+ "LOAD_FAST_LOAD_FAST" => {
192
+ // Python 3.12+ optimization: loads two variables at once
193
+ // argval is a tuple of two variable names like ('a', 'b')
194
+ let var_names: Vec<String> = argval.extract()?;
195
+ let code = callable.getattr("__code__")?;
196
+ let varnames: Vec<String> = code.getattr("co_varnames")?.extract()?;
197
+ for var_name in var_names {
198
+ if let Some(idx) = varnames.iter().position(|n| n == &var_name) {
199
+ stack.push(BytecodeValue::Param(idx as u32));
200
+ } else {
201
+ return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
202
+ "Cannot analyze lambda: unknown variable '{}'. Lambda parameters must be used directly.",
203
+ var_name
204
+ )));
205
+ }
183
206
  }
184
207
  }
185
208
  "LOAD_ATTR" | "LOAD_METHOD" => {
@@ -1361,6 +1384,79 @@ mod tests {
1361
1384
  });
1362
1385
  }
1363
1386
 
1387
+ #[test]
1388
+ fn test_analyze_bi_lambda_direct_param_add() {
1389
+ // Tests LOAD_FAST_LOAD_FAST bytecode (Python 3.12+)
1390
+ // This is used by compose() combiner lambdas like: lambda a, b: a + b
1391
+ init_python();
1392
+ Python::attach(|py| {
1393
+ let locals = PyDict::new(py);
1394
+ py.run(c"f = lambda a, b: a + b", None, Some(&locals))
1395
+ .unwrap();
1396
+ let func = locals.get_item("f").unwrap().unwrap();
1397
+
1398
+ let info = LambdaInfo {
1399
+ callable: func.unbind(),
1400
+ name: "test".to_string(),
1401
+ param_count: 2,
1402
+ class_hint: None,
1403
+ expression: None,
1404
+ };
1405
+
1406
+ let result = analyze_lambda(py, &info).unwrap();
1407
+
1408
+ match result {
1409
+ Expression::Add { left, right } => {
1410
+ assert!(matches!(*left, Expression::Param { index: 0 }));
1411
+ assert!(matches!(*right, Expression::Param { index: 1 }));
1412
+ }
1413
+ _ => panic!("Expected Add expression, got {:?}", result),
1414
+ }
1415
+ });
1416
+ }
1417
+
1418
+ #[test]
1419
+ fn test_analyze_tri_lambda_arithmetic() {
1420
+ // Tests three-parameter lambda with arithmetic
1421
+ init_python();
1422
+ Python::attach(|py| {
1423
+ let locals = PyDict::new(py);
1424
+ py.run(c"f = lambda a, b, c: a + b + c", None, Some(&locals))
1425
+ .unwrap();
1426
+ let func = locals.get_item("f").unwrap().unwrap();
1427
+
1428
+ let info = LambdaInfo {
1429
+ callable: func.unbind(),
1430
+ name: "test".to_string(),
1431
+ param_count: 3,
1432
+ class_hint: None,
1433
+ expression: None,
1434
+ };
1435
+
1436
+ let result = analyze_lambda(py, &info).unwrap();
1437
+
1438
+ // Should be Add(Add(a, b), c)
1439
+ match result {
1440
+ Expression::Add { left, right } => {
1441
+ // right should be Param 2 (c)
1442
+ assert!(matches!(*right, Expression::Param { index: 2 }));
1443
+ // left should be Add(a, b)
1444
+ match *left {
1445
+ Expression::Add {
1446
+ left: l2,
1447
+ right: r2,
1448
+ } => {
1449
+ assert!(matches!(*l2, Expression::Param { index: 0 }));
1450
+ assert!(matches!(*r2, Expression::Param { index: 1 }));
1451
+ }
1452
+ _ => panic!("Expected nested Add"),
1453
+ }
1454
+ }
1455
+ _ => panic!("Expected Add expression, got {:?}", result),
1456
+ }
1457
+ });
1458
+ }
1459
+
1364
1460
  #[test]
1365
1461
  fn test_analyze_nested_field_access() {
1366
1462
  init_python();