@@ -60,8 +60,11 @@ class RandVar:
6060 specified.
6161 :param constraints: List or tuple of constraints that apply to this random variable.
6262 Each of these apply across the values in the list, if a length is specified.
63- :param length: Specify a length > 0 to turn this variable into a list of random
64- values. A value of 0 means a scalar value. A value >= 1 means a list of that length.
63+ :param length: Specify a length >= 0 to turn this variable into a list of random
64+ values. A value >= 0 means a list of that length. A zero-length list is just
65+ an empty list. A value of ``None`` (default) means a scalar value.
66+ :param rand_length: Specify the name of a random variable that defines the length
67+ of this variable. The variable must have already been added to this instance.
6568 :param max_iterations: The maximum number of failed attempts to solve the randomization
6669 problem before giving up.
6770 :param max_domain_size: The maximum size of domain that a constraint satisfaction problem
@@ -82,7 +85,8 @@ def __init__(self,
8285 args : Optional [tuple ]= None ,
8386 constraints : Optional [Iterable [utils .Constraint ]]= None ,
8487 list_constraints : Optional [Iterable [utils .Constraint ]]= None ,
85- length : int ,
88+ length : Optional [int ]= None ,
89+ rand_length : Optional [str ]= None ,
8690 max_iterations : int ,
8791 max_domain_size : int ,
8892 disable_naive_list_solver : bool ,
@@ -91,9 +95,14 @@ def __init__(self,
9195 self .name = name
9296 self .order = order
9397 self .length = length
98+ self .rand_length = rand_length
99+ self .rand_length_val = None
100+ assert not ((length is not None ) and (rand_length is not None )), \
101+ "length and rand_length are mutually-exclusive, but both were specified"
94102 self .max_iterations = max_iterations
95103 self .max_domain_size = max_domain_size
96- assert ((domain is not None ) != (fn is not None )) != (bits is not None ), "Must specify exactly one of fn, domain or bits"
104+ assert ((domain is not None ) != (fn is not None )) != (bits is not None ), \
105+ "Must specify exactly one of fn, domain or bits"
97106 if fn is None :
98107 assert args is None , "args has no effect without fn"
99108 self .domain = domain
@@ -219,7 +228,8 @@ def add_constraint(self, constr: utils.Constraint) -> None:
219228
220229 :param constr: Constraint to add.
221230 '''
222- if self .length > 0 :
231+ length = self .get_length ()
232+ if length is not None :
223233 # Treat all additional constraints as list constraints,
224234 # although this is a little less performant.
225235 self .list_constraints .append (constr )
@@ -230,6 +240,35 @@ def add_constraint(self, constr: utils.Constraint) -> None:
230240 self .check_constraints = True
231241 self .randomizer = self .create_randomizer ()
232242
243+ def get_length (self ) -> int :
244+ '''
245+ Function to get the length of the random list.
246+
247+ :return: The length of the list.
248+ '''
249+ if self .rand_length is None :
250+ return self .length
251+ if self .rand_length_val is None :
252+ raise RuntimeError ("RandVar was marked as having a random length," \
253+ " but none was given when get_length was called." )
254+ return self .rand_length_val
255+
256+ def set_rand_length (self , length : int ) -> None :
257+ '''
258+ Function to set the random length.
259+
260+ Should only be used when this ``RandVar``
261+ instance is indicated to have a random length
262+ that depends on another variable.
263+
264+ :raises RuntimeError: If this variable instance is not
265+ marked as one with a random length.
266+ '''
267+ if self .rand_length is None :
268+ raise RuntimeError ("RandVar was not marked as having a random length," \
269+ " but set_rand_length was called." )
270+ self .rand_length_val = length
271+
233272 def _get_random (self ) -> random .Random :
234273 '''
235274 Internal function to get the appropriate randomization object.
@@ -254,14 +293,20 @@ def get_domain_size(self) -> int:
254293 # of this variable. Return 1.
255294 return 1
256295 else :
257- # length == 0 implies a scalar variable, 1 is a list of length 1
258- if self .length == 0 or self .length == 1 :
296+ length = self .get_length ()
297+ if length is None :
298+ # length is None implies a scalar variable.
299+ return len (self .domain )
300+ elif length == 0 :
301+ # This is a zero-length list, adding no complexity.
302+ return 1
303+ elif length == 1 :
259304 return len (self .domain )
260305 else :
261306 # In this case it is effectively cartesian product, i.e.
262307 # n ** k, where n is the size of the domain and k is the length
263308 # of the list.
264- return len (self .domain ) ** self . length
309+ return len (self .domain ) ** length
265310
266311 def can_use_with_constraint (self ) -> bool :
267312 '''
@@ -284,18 +329,22 @@ def get_constraint_domain(self) -> utils.Domain:
284329 :return: the variable's domain in a format that will work
285330 with the ``constraint`` package.
286331 '''
287- if self .length == 0 :
332+ length = self .get_length ()
333+ if length is None :
288334 # Straightforward, scalar
289335 return self .domain
290- elif self .length == 1 :
336+ elif length == 0 :
337+ # List of length zero - an empty list is only correct choice.
338+ return [[]]
339+ elif length == 1 :
291340 # List of length one
292341 return [[x ] for x in self .domain ]
293342 else :
294343 # List of greater length, cartesian product.
295344 # Beware that this may be an extremely large domain.
296345 # Ensure each element is of type list, which is what
297346 # we want to return.
298- return [list (x ) for x in product (self .domain , repeat = self . length )]
347+ return [list (x ) for x in product (self .domain , repeat = length )]
299348
300349 def randomize_once (self , constraints : Iterable [utils .Constraint ], check_constraints : bool , debug : bool ) -> Any :
301350 '''
@@ -381,7 +430,7 @@ def randomize_list_naive(
381430 constraints : Iterable [utils .Constraint ],
382431 check_constraints : bool ,
383432 list_constraints : Iterable [utils .Constraint ],
384- debug : bool ,
433+ debug : bool ,
385434 debug_fail : Optional [RandomizationFail ],
386435 ):
387436 '''
@@ -398,11 +447,13 @@ def randomize_list_naive(
398447 all debug info along the way and not just the final failure.
399448 :param debug_fail: :class:`RandomizationFail` containing debug info,
400449 if in debug mode, else ``None``.
450+
401451 :return: A random list of values for the variable, respecting
402452 the constraints.
403453 '''
454+ length = self .get_length ()
404455 values = [self .randomize_once (constraints , check_constraints , debug ) \
405- for _ in range (self . length )]
456+ for _ in range (length )]
406457 values_valid = len (list_constraints ) == 0
407458 iterations = 0
408459 max_iterations = self .max_iterations
@@ -421,7 +472,7 @@ def randomize_list_naive(
421472 debug_fail .add_values (iterations , {self .name : values })
422473 iterations += 1
423474 values = [self .randomize_once (constraints , check_constraints , debug ) \
424- for _ in range (self . length )]
475+ for _ in range (length )]
425476 return values
426477
427478 def randomize_list_subset (
@@ -451,13 +502,14 @@ def randomize_list_subset(
451502 :raises RandomizationError: When the problem cannot be solved in fewer than
452503 the allowed number of iterations.
453504 '''
505+ length = self .get_length ()
454506 values = [self .randomize_once (constraints , check_constraints , debug ) \
455- for _ in range (self . length )]
507+ for _ in range (length )]
456508 values_valid = len (list_constraints ) == 0
457509 iterations = 0
458510 # Allow more attempts at a list, as it may be computationally hard.
459511 # Assume it's linearly harder.
460- max_iterations = self .max_iterations * self . length
512+ max_iterations = self .max_iterations * length
461513 checked = []
462514 while not values_valid :
463515 iterations += 1
@@ -473,7 +525,7 @@ def randomize_list_subset(
473525 raise utils .RandomizationError ("Too many iterations, can't solve problem" , debug_info )
474526 # Keep a subset of the answer, to try to ensure forward progress.
475527 min_group_size = len (checked ) + 1
476- for idx in range (min_group_size , self . length ):
528+ for idx in range (min_group_size , length ):
477529 tmp_values = values [:idx ]
478530 problem = constraint .Problem ()
479531 problem .addVariable (self .name , (tmp_values ,))
@@ -493,7 +545,7 @@ def randomize_list_subset(
493545 # degrees of freedom.
494546 checked = tmp_values
495547 values = checked + [self .randomize_once (constraints , check_constraints , debug ) \
496- for _ in range (self . length - len (checked ))]
548+ for _ in range (length - len (checked ))]
497549 problem = constraint .Problem ()
498550 problem .addVariable (self .name , (values ,))
499551 for con in list_constraints :
@@ -507,7 +559,7 @@ def randomize_list_subset(
507559 def randomize (
508560 self ,
509561 temp_constraints : Optional [Iterable [utils .Constraint ]]= None ,
510- debug : bool = False
562+ debug : bool = False ,
511563 ) -> Any :
512564 '''
513565 Returns a random value based on the definition of this random variable.
@@ -525,7 +577,8 @@ def randomize(
525577 # adding any temporary ones in.
526578 constraints = list (self .constraints )
527579 using_temp_constraints = temp_constraints is not None and len (temp_constraints ) > 0
528- if self .length == 0 :
580+ length = self .get_length ()
581+ if length is None :
529582 # Interpret temporary constraints as scalar constraints
530583 if using_temp_constraints :
531584 check_constraints = True
0 commit comments