4
4
import constraint
5
5
import random
6
6
from collections import defaultdict
7
- from typing import Any , Callable , Dict , Iterable , List , Optional
7
+ from typing import Any , Callable , Dict , Iterable , List , Optional , Set
8
8
9
9
from . import utils
10
10
from .internal .multivar import MultiVarProblem
@@ -59,18 +59,19 @@ def __init__(
59
59
max_domain_size : int = utils .CONSTRAINT_MAX_DOMAIN_SIZE ,
60
60
) -> None :
61
61
# Prefix 'internal use' variables with '_', as randomized results are populated to the class
62
- self ._random = _random
63
- self ._random_vars = {}
64
- self ._rand_list_lengths = defaultdict (list )
65
- self ._constraints : List [utils .ConstraintAndVars ] = []
66
- self ._constrained_vars = set ()
67
- self ._max_iterations = max_iterations
68
- self ._max_domain_size = max_domain_size
69
- self ._naive_solve = True
70
- self ._sparse_solve = True
71
- self ._sparsities = [1 , 10 , 100 , 1000 ]
72
- self ._thorough_solve = True
73
- self ._problem_changed = False
62
+ self ._random : Optional [random .Random ] = _random
63
+ self ._random_vars : Dict [str , RandVar ] = {}
64
+ self ._rand_list_lengths : Dict [str , List [str ]]= defaultdict (list )
65
+ self ._constraints : List [utils .ConstraintAndVars ] = []
66
+ self ._constrained_vars : Set [str ] = set ()
67
+ self ._max_iterations : int = max_iterations
68
+ self ._max_domain_size : int = max_domain_size
69
+ self ._naive_solve : bool = True
70
+ self ._sparse_solve : bool = True
71
+ self ._sparsities : List [int ] = [1 , 10 , 100 , 1000 ]
72
+ self ._thorough_solve : bool = True
73
+ self ._problem_changed : bool = False
74
+ self ._multi_var_problem : Optional [MultiVarProblem ] = None
74
75
75
76
def _get_random (self ) -> random .Random :
76
77
'''
@@ -85,6 +86,29 @@ def _get_random(self) -> random.Random:
85
86
return random
86
87
return self ._random
87
88
89
+ def _get_list_length_constraints (self , var_names : Set [str ]) -> List [utils .ConstraintAndVars ]:
90
+ '''
91
+ Internal function to get constraints to describe
92
+ the relationship between random list lengths and the variables
93
+ that define them.
94
+
95
+ :param var_names: List of variable names that we want to
96
+ constrain. Only consider variables from within
97
+ this list in the result. Both the list length
98
+ variable and the list variable it constrains must
99
+ be in ``var_names`` to return a constraint.
100
+ :return: List of constraints with variables, describing
101
+ relationship between random list variables and lengths.
102
+ '''
103
+ result : List [utils .ConstraintAndVars ] = []
104
+ for rand_list_length , list_vars in self ._rand_list_lengths .items ():
105
+ if rand_list_length in var_names :
106
+ for list_var in list_vars :
107
+ if list_var in var_names :
108
+ len_constr = lambda _list_var , _length : len (_list_var ) == _length
109
+ result .append ((len_constr , (list_var , rand_list_length )))
110
+ return result
111
+
88
112
def set_solver_mode (
89
113
self ,
90
114
* ,
@@ -100,7 +124,7 @@ def set_solver_mode(
100
124
1. Naive solve - randomizing and checking constraints.
101
125
For some problems, it is more expedient to skip this
102
126
step and go straight to a MultiVarProblem.
103
- 2. Sparse solve - graph-based exporation of state space.
127
+ 2. Sparse solve - graph-based exploration of state space.
104
128
Start with depth-first search, move to wider subsets
105
129
of each level of state space until valid solution
106
130
found.
@@ -352,38 +376,38 @@ def randomize(
352
376
result = {}
353
377
354
378
# Copy always-on constraints, ready to add any temporary ones
355
- constraints = list (self ._constraints )
356
- constrained_vars = set (self ._constrained_vars )
379
+ constraints : Set [ utils . ConstraintAndVars ] = list (self ._constraints )
380
+ constrained_var_names : Set [ str ] = set (self ._constrained_vars )
357
381
358
382
# Process temporary constraints
359
- tmp_single_var_constraints = defaultdict (list )
383
+ tmp_single_var_constraints : Dict [ str , List [ utils . Constraint ]] = defaultdict (list )
360
384
# Set to True if the problem is different from the base problem
361
385
problem_changed = False
362
386
if with_constraints is not None :
363
- for constr , vars in with_constraints :
364
- if not isinstance (vars , Iterable ):
387
+ for constr , var_names in with_constraints :
388
+ if not isinstance (var_names , Iterable ):
365
389
raise TypeError ("with_constraints should specify a list of tuples of (constraint, Iterable[variables])" )
366
- if not len (vars ) > 0 :
390
+ if not len (var_names ) > 0 :
367
391
raise ValueError ("Cannot add a constraint that applies to no variables" )
368
- if len (vars ) == 1 :
392
+ if len (var_names ) == 1 :
369
393
# Single-variable constraint
370
- tmp_single_var_constraints [vars [0 ]].append (constr )
394
+ tmp_single_var_constraints [var_names [0 ]].append (constr )
371
395
problem_changed = True
372
396
else :
373
397
# Multi-variable constraint
374
- constraints .append ((constr , vars ))
375
- for var in vars :
376
- constrained_vars .add (var )
398
+ constraints .append ((constr , var_names ))
399
+ for var_name in var_names :
400
+ constrained_var_names .add (var_name )
377
401
problem_changed = True
378
402
# If a variable becomes constrained due to temporary multi-variable
379
403
# constraints, we must respect single var temporary constraints too.
380
- for var , constrs in sorted (tmp_single_var_constraints .items ()):
381
- if var in constrained_vars :
404
+ for var_name , constrs in sorted (tmp_single_var_constraints .items ()):
405
+ if var_name in constrained_var_names :
382
406
for constr in constrs :
383
- constraints .append ((constr , (var ,)))
407
+ constraints .append ((constr , (var_name ,)))
384
408
385
409
# Don't allow non-determinism when iterating over a set
386
- constrained_vars = sorted (constrained_vars )
410
+ constrained_var_names = sorted (constrained_var_names )
387
411
# Don't allow non-determinism when iterating over a dict
388
412
random_var_names = sorted (self ._random_vars .keys ())
389
413
list_length_names = sorted (self ._rand_list_lengths .keys ())
@@ -423,8 +447,8 @@ def randomize(
423
447
if attempts == max :
424
448
break
425
449
problem = constraint .Problem ()
426
- for var in constrained_vars :
427
- problem .addVariable (var , (result [var ],))
450
+ for var_name in constrained_var_names :
451
+ problem .addVariable (var_name , (result [var_name ],))
428
452
for _constraint , variables in constraints :
429
453
problem .addConstraint (_constraint , variables )
430
454
solutions = problem .getSolutions ()
@@ -439,7 +463,7 @@ def randomize(
439
463
for list_length_name in list_length_names :
440
464
# If the length-defining variable is constrained,
441
465
# re-randomize it and all its dependent vars.
442
- if list_length_name not in with_values and list_length_name in constrained_vars :
466
+ if list_length_name not in with_values and list_length_name in constrained_var_names :
443
467
tmp_constraints = tmp_single_var_constraints .get (list_length_name , [])
444
468
length_result = self ._random_vars [list_length_name ].randomize (tmp_constraints , debug )
445
469
result [list_length_name ] = length_result
@@ -449,7 +473,7 @@ def randomize(
449
473
self ._random_vars [dependent_var_name ].set_rand_length (length_result )
450
474
tmp_constraints = tmp_single_var_constraints .get (dependent_var_name , [])
451
475
result [dependent_var_name ] = self ._random_vars [dependent_var_name ].randomize (tmp_constraints , debug )
452
- for var in constrained_vars :
476
+ for var in constrained_var_names :
453
477
# Don't re-randomize if we've specified a concrete value
454
478
if var in with_values :
455
479
continue
@@ -458,7 +482,7 @@ def randomize(
458
482
continue
459
483
# Don't re-randomize list vars which have been re-randomized once already.
460
484
rand_length = self ._random_vars [var ].rand_length
461
- if rand_length is not None and rand_length in constrained_vars :
485
+ if rand_length is not None and rand_length in constrained_var_names :
462
486
continue
463
487
tmp_constraints = tmp_single_var_constraints .get (var , [])
464
488
result [var ] = self ._random_vars [var ].randomize (tmp_constraints , debug )
@@ -473,10 +497,14 @@ def randomize(
473
497
' There is no way to solve the problem.'
474
498
)
475
499
if problem_changed or self ._problem_changed or self ._multi_var_problem is None :
500
+ # Add list length constraints here.
501
+ # By this point, we have failed to get a naive solution,
502
+ # so we need the list lengths as proper constraints.
503
+ constraints += self ._get_list_length_constraints (constrained_var_names )
476
504
multi_var_problem = MultiVarProblem (
477
- self ,
478
- [self ._random_vars [var_name ] for var_name in constrained_vars ],
479
- constraints ,
505
+ random_getter = self . _get_random ,
506
+ vars = [self ._random_vars [var_name ] for var_name in constrained_var_names ],
507
+ constraints = constraints ,
480
508
max_iterations = self ._max_iterations ,
481
509
max_domain_size = self ._max_domain_size ,
482
510
)
0 commit comments