Skip to content

API Reference

ParameterBank

A collection of parameters with sampling, constraints, and conversions.

The bank stores independent and derived parameters, optional constraint functions, and a canonical parameter order. It can sample full parameter instances, validate them against constraints, and convert between rich ParameterSet and array/dataframe representations.

Parameters:

Name Type Description Default
parameters dict[str, IndependentScalarParameter | IndependentVectorParameter | DerivedScalarParameter] | None

Dictionary mapping parameter names to parameter instances.

None
constraints list[Callable[[ParameterSet], bool]] | None

List of constraint functions that take a ParameterSet and return a boolean.

None
array_mode bool

If True, sampling and conversions use only sampled parameters and return plain arrays; otherwise use all parameters and return ParameterSet objects.

False
texnames dict[str, str] | None

Optional dictionary mapping parameter names to TeX-formatted display names.

None
max_attempts int

Maximum number of attempts when sampling with constraints before raising an error. Defaults to 100.

100
Source code in jscip/parameter_bank.py
 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
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
class ParameterBank:
    """A collection of parameters with sampling, constraints, and conversions.

    The bank stores independent and derived parameters, optional constraint
    functions, and a canonical parameter order. It can sample full parameter
    instances, validate them against constraints, and convert between rich
    ``ParameterSet`` and array/dataframe representations.

    Args:
        parameters: Dictionary mapping parameter names to parameter instances.
        constraints: List of constraint functions that take a ParameterSet and
            return a boolean.
        array_mode: If True, sampling and conversions use only sampled
            parameters and return plain arrays; otherwise use all parameters
            and return ParameterSet objects.
        texnames: Optional dictionary mapping parameter names to TeX-formatted
            display names.
        max_attempts: Maximum number of attempts when sampling with constraints
            before raising an error. Defaults to 100.
    """

    def __init__(
        self,
        parameters: (
            dict[
                str,
                IndependentScalarParameter | IndependentVectorParameter | DerivedScalarParameter,
            ]
            | None
        ) = None,
        constraints: list[Callable[[ParameterSet], bool]] | None = None,
        array_mode: bool = False,
        texnames: dict[str, str] | None = None,
        max_attempts: int = 100,
    ) -> None:
        self.parameters = parameters if parameters is not None else {}
        self.constraints = constraints if constraints is not None else []
        self.array_mode = array_mode
        if not isinstance(max_attempts, int) or max_attempts < 1:
            raise ValueError("max_attempts must be a positive integer.")
        self._max_attempts = max_attempts
        self.texnames = texnames if texnames is not None else {}

        for key, value in self.parameters.items():
            if not isinstance(
                value,
                (
                    IndependentScalarParameter,
                    IndependentVectorParameter,
                    DerivedScalarParameter,
                    DerivedVectorParameter,
                ),
            ):
                raise ValueError(
                    f"Value for key '{key}' must be an instance of "
                    f"IndependentScalarParameter, IndependentVectorParameter, "
                    f"DerivedScalarParameter, or DerivedVectorParameter."
                )

        # Validate texnames keys match parameter names
        if self.texnames:
            invalid_keys = set(self.texnames.keys()) - set(self.parameters.keys())
            if invalid_keys:
                raise ValueError(
                    f"texnames contains keys not in parameters: {invalid_keys}. "
                    f"Valid parameter names are: {set(self.parameters.keys())}"
                )

        # compute indices of sampled parameters in the canonical order
        self._refresh_sampled_indices()
        logger.debug(
            "Initialized ParameterBank with %d parameters and %d constraints",
            len(self.parameters),
            len(self.constraints),
        )

    def _refresh_sampled_indices(self) -> None:
        """Refresh cached indices for sampled parameters based on current parameters."""
        self.sampled_indices = [
            self.names.index(key)
            for key, param in self.parameters.items()
            if isinstance(param, (IndependentScalarParameter, IndependentVectorParameter))
            and param.is_sampled
        ]
        logger.debug("Refreshed sampled indices: %s", self.sampled_indices)

    @property
    def names(self) -> list[str]:
        """Get the names of all parameters in the bank.
        This also defines the canonical order of the parameters."""
        return list(self.parameters.keys())

    @property
    def sampled(self) -> list[str]:
        """Get a list of all parameters that are set to be sampled."""
        return [key for key, param in self.parameters.items() if param.is_sampled]

    @property
    def vector_names(self) -> list[str]:
        """Get a list of all vector parameter names."""
        return [
            key
            for key, param in self.parameters.items()
            if isinstance(param, IndependentVectorParameter)
        ]

    @property
    def lower_bounds(self) -> np.ndarray:
        """Get the lower bounds of all sampled parameters.

        For scalar parameters, returns the scalar lower bound.
        For vector parameters, returns the lower bound array or scalar if uniform.
        """
        bounds = []
        for _key, param in self.parameters.items():
            if isinstance(param, IndependentScalarParameter) and param.is_sampled:
                assert param.range is not None  # Type guard for mypy
                bounds.append(param.range[0])
            elif isinstance(param, IndependentVectorParameter) and param.is_sampled:
                # Vector parameter range can be tuple of arrays or scalars
                assert param.range is not None  # Type guard for mypy
                if isinstance(param.range[0], np.ndarray):
                    bounds.extend(param.range[0])
                else:
                    bounds.extend([param.range[0]] * param.shape[0])
        return np.array(bounds)

    @property
    def upper_bounds(self) -> np.ndarray:
        """Get the upper bounds of all sampled parameters.

        For scalar parameters, returns the scalar upper bound.
        For vector parameters, returns the upper bound array or scalar if uniform.
        """
        bounds = []
        for _key, param in self.parameters.items():
            if isinstance(param, IndependentScalarParameter) and param.is_sampled:
                assert param.range is not None  # Type guard for mypy
                bounds.append(param.range[1])
            elif isinstance(param, IndependentVectorParameter) and param.is_sampled:
                # Vector parameter range can be tuple of arrays or scalars
                assert param.range is not None  # Type guard for mypy
                if isinstance(param.range[1], np.ndarray):
                    bounds.extend(param.range[1])
                else:
                    bounds.extend([param.range[1]] * param.shape[0])
        return np.array(bounds)

    @property
    def sampled_texnames(self) -> list[str]:
        """Get the TeX names of all sampled parameters."""
        return [self.texnames.get(key, key) for key in self.sampled]

    def __repr__(self) -> str:
        return f"ParameterBank(parameters={self.parameters}, constraints={self.constraints})"

    def __contains__(self, key: str) -> bool:
        """Check if a parameter exists in the bank."""
        return key in self.parameters

    def __len__(self) -> int:
        """Get the number of parameters in the bank."""
        return len(self.parameters)

    def __iter__(self) -> Iterator[str]:
        """Iterate over the parameter names in the bank."""
        return iter(self.parameters)

    def __getitem__(
        self, key: str
    ) -> IndependentScalarParameter | IndependentVectorParameter | DerivedScalarParameter:
        """Get a parameter by its name."""
        if key in self.parameters:
            return self.parameters[key]
        else:
            raise KeyError(f"Parameter '{key}' not found in the bank.")

    def copy(self) -> ParameterBank:
        """Create a copy of the ParameterBank."""
        result = ParameterBank(
            parameters={k: v.copy() for k, v in self.parameters.items()},
            constraints=self.constraints.copy(),
            array_mode=self.array_mode,
            texnames=self.texnames.copy(),
            max_attempts=self._max_attempts,
        )
        logger.debug("Copied ParameterBank: %s", result)
        return result

    def get_value(self, key: str) -> float | np.ndarray:
        if key in self.parameters:
            param = self.parameters[key]
            if isinstance(param, (IndependentScalarParameter, IndependentVectorParameter)):
                return param.value
            else:
                raise ValueError(
                    f"Parameter '{key}' is a derived parameter and has no fixed value."
                )
        else:
            raise KeyError(f"Parameter '{key}' not found in the bank.")

    def merge(self, other: ParameterBank) -> None:
        """Merge another ParameterBank into this one.
        If a parameter with the same name exists, it will be overwritten.
        """
        if not isinstance(other, ParameterBank):
            raise ValueError("Other must be an instance of ParameterBank.")
        for key, value in other.parameters.items():
            self.parameters[key] = value.copy()
        self.constraints.extend(other.constraints)
        self._refresh_sampled_indices()
        logger.debug("Merged ParameterBank: %s", self)

    def add_parameter(
        self,
        name: str,
        parameter: (
            IndependentScalarParameter
            | IndependentVectorParameter
            | DerivedScalarParameter
            | DerivedVectorParameter
        ),
    ) -> None:
        """Add a new parameter to the bank."""
        if not isinstance(
            parameter,
            (
                IndependentScalarParameter,
                IndependentVectorParameter,
                DerivedScalarParameter,
            ),
        ):
            raise ValueError(
                "Parameter must be an instance of IndependentScalarParameter, "
                "IndependentVectorParameter, or DerivedScalarParameter."
            )
        if name in self.parameters:
            raise KeyError(f"Parameter '{name}' already exists in the bank.")
        self.parameters[name] = parameter
        self._refresh_sampled_indices()
        logger.debug("Added parameter '%s' to ParameterBank: %s", name, self)

    def add_constraint(self, constraint: Callable[[ParameterSet], bool]) -> None:
        """Add a new constraint to the bank."""
        if not callable(constraint):
            raise ValueError("Constraint must be a callable function.")
        self.constraints.append(constraint)
        logger.debug("Added constraint '%s' to ParameterBank: %s", constraint, self)

    def get_constraints(self) -> list[Callable[[ParameterSet], bool]]:
        """Get all constraints in the bank."""
        return self.constraints

    def get_default_values(self, return_array: bool | None = None) -> ParameterSet | np.ndarray:
        """Return default values for all parameters.

        Computes a ``ParameterSet`` by taking the current ``value`` for all
        independent parameters and computing all derived parameters from those
        values. Optionally, returns the sampled subset as a NumPy array when
        ``return_array=True``.

        Args:
            return_array: If True, return a 1D NumPy array of sampled parameter
                values in canonical sampled order. If False, return a full
                ``ParameterSet``. Defaults to ``self.array_mode``.

        Returns:
            ParameterSet | numpy.ndarray: The default instance or the sampled
            values array.

        Raises:
            ValueError: If ``return_array`` is not a boolean.
        """
        if return_array is None:
            return_array = self.array_mode
        if not isinstance(return_array, bool):
            raise ValueError("return_array must be a boolean value.")
        p = ParameterSet(
            {
                key: param.value
                for key, param in self.parameters.items()
                if isinstance(param, (IndependentScalarParameter, IndependentVectorParameter))
            }
        )
        logger.debug(
            "[get_default_values] Default values for all independent parameters in the bank: %s",
            p,
        )
        p = ParameterSet(
            {
                **p,
                **{
                    key: param.compute(p)
                    for key, param in self.parameters.items()
                    if isinstance(param, DerivedScalarParameter)
                },
            }
        )
        p = self.order(p)
        logger.debug("[get_default_values] Default values for all parameters in the bank: %s", p)
        if return_array:
            return self.instance_to_array(p)
        else:
            return p

    def instance_to_array(self, input: ParameterSet | list[ParameterSet]) -> np.ndarray:
        """Convert a parameter instance (or list) to a sampled parameter array.

        Args:
            input: A single ``ParameterSet`` or list of ``ParameterSet``
                instances.

        Returns:
            numpy.ndarray: 1D array for a single instance or 2D array for a
            list of instances, containing values for sampled parameters only,
            in canonical sampled order. Vector parameters are flattened into
            the array.

        Raises:
            ValueError: If ``input`` is not a ``ParameterSet`` or list thereof.
        """
        if not isinstance(input, (ParameterSet, list)):
            raise ValueError(
                "Input must be a ParameterSetInstance or a list of ParameterSetInstances."
            )
        if isinstance(input, ParameterSet):
            # Flatten vector parameters into the theta array
            theta_values: list[float] = []
            for key in self.sampled:
                value = input[key]
                if isinstance(value, np.ndarray):
                    theta_values.extend(value)
                else:
                    theta_values.append(value)
            theta = np.array(theta_values)
            logger.debug("[instance_to_array] Converted ParameterSet to numpy array: %s", theta)
        else:
            # return a 2D array of shape (n_instances, n_theta_dims)
            theta_list = []
            for instance in input:
                theta_values = []
                for key in self.sampled:
                    value = instance[key]
                    if isinstance(value, np.ndarray):
                        theta_values.extend(value)
                    else:
                        theta_values.append(value)
                theta_list.append(theta_values)
            theta = np.array(theta_list)
            logger.debug(
                "[instance_to_array] Converted list of ParameterSetInstances to numpy array: %s",
                theta,
            )
        return theta

    def dataframe_to_array(self, df: pd.DataFrame) -> np.ndarray:
        """Extract sampled parameter columns from a DataFrame as a NumPy array.

        Args:
            df: DataFrame containing sampled parameter columns.

        Returns:
            numpy.ndarray: 2D array of sampled values in canonical sampled
            order.

        Raises:
            ValueError: If ``df`` is not a pandas DataFrame.
        """
        if not isinstance(df, pd.DataFrame):
            raise ValueError("Input must be a pandas DataFrame.")
        theta = df[self.sampled].to_numpy()
        return theta

    def array_to_instance(self, theta: np.ndarray) -> ParameterSet:
        """Convert a parameter array to a parameter instance.

        When ``array_mode`` is True, ``theta`` must contain only sampled
        independent parameters in canonical sampled order. Otherwise, it must
        contain values for all independent parameters in canonical order.

        Args:
            theta: 1D NumPy array.

        Returns:
            ParameterSet: A full instance with derived parameters recomputed.

        Raises:
            ValueError: If shapes are inconsistent with ``array_mode`` or
                if ``theta`` is not a NumPy array.
        """
        if not isinstance(theta, np.ndarray):
            raise ValueError("Input must be a numpy array, instead got: " + str(type(theta)))
        # validate length depending on array_mode
        if self.array_mode:
            # Calculate expected theta length (accounting for vector parameters)
            expected_len = len(self.lower_bounds)  # This already accounts for vectors
            if len(theta) != expected_len:
                raise ValueError(
                    f"Array length {len(theta)} does not match expected theta dimensions {expected_len}."
                )
        else:
            if len(theta) != len(self.parameters):
                raise ValueError(
                    f"Array length {len(theta)} does not match number of parameters {len(self.parameters)}."
                )
        # theta in this case must be a 1D array
        # Start with defaults
        out = self.get_default_values(return_array=False)
        if self.array_mode:
            # theta provides only sampled independent parameters
            # Need to unflatten vector parameters
            theta_idx = 0
            for key in self.sampled:
                param = self.parameters[key]
                if isinstance(param, IndependentVectorParameter):
                    # Extract vector elements from theta
                    n_elements = param.shape[0]
                    out[key] = theta[theta_idx : theta_idx + n_elements]
                    theta_idx += n_elements
                else:
                    # Scalar parameter
                    out[key] = theta[theta_idx]
                    theta_idx += 1
        else:
            # theta provides values for ALL parameters in canonical order
            if len(theta) != len(self.parameters):
                raise ValueError(
                    f"Array length {len(theta)} does not match number of parameters {len(self.parameters)}."
                )
            for i, key in enumerate(self.names):
                param = self.parameters[key]
                if isinstance(param, IndependentScalarParameter):
                    out[key] = float(theta[i])
        # recompute derived parameters
        assert isinstance(out, ParameterSet)  # Type guard for mypy
        out = ParameterSet(
            {
                **out,
                **{
                    key: param.compute(out)
                    for key, param in self.parameters.items()
                    if isinstance(param, DerivedScalarParameter)
                },
            }
        )
        return out

    def _sample_once(self) -> ParameterSet:
        """Sample a single full parameter set (internal).

        Samples all sampled independent parameters (scalar and vector), computes
        derived values, and returns a ``ParameterSet`` ordered canonically.
        """
        # first, sample all independent parameters (both sampled and fixed)
        p = ParameterSet(
            {
                key: param.sample()
                for key, param in self.parameters.items()
                if isinstance(param, (IndependentScalarParameter, IndependentVectorParameter))
            }
        )
        logger.debug(
            "[sample_once] Sampled values for all independent parameters in the bank: %s",
            p,
        )
        # then, compute all derived parameters based on the sampled independent parameters
        for key, param in self.parameters.items():
            if isinstance(param, (DerivedScalarParameter, DerivedVectorParameter)):
                p[key] = param.compute(p)
        logger.debug("[sample_once] Sampled values for all parameters in the bank: %s", p)
        # put result in canonical order according to self.canonical_order
        p = self.order(p)
        return p

    def _sample_once_constrained(self) -> ParameterSet:
        """Sample a single parameter set that satisfies all constraints (internal)."""
        attempts = 0
        while attempts < self._max_attempts:
            attempts += 1
            sample = self._sample_once()
            # Check if the sample meets all constraints
            if all(sample.satisfies(c) for c in self.constraints):
                return sample
        raise RuntimeError(
            f"Failed to sample a parameter set satisfying constraints after {self._max_attempts} attempts."
        )

    def sample(self, size: int | tuple | None = None) -> ParameterSet | pd.DataFrame | np.ndarray:
        """Sample parameter sets or theta arrays.

        Args:
            size: If ``None``, returns a single instance. If ``int``, returns a
                batch. If ``tuple``, returns product size; multi-d shapes are
                only supported when ``array_mode`` is True.

        Returns:
            ParameterSet | pandas.DataFrame | numpy.ndarray: Depending on
            ``array_mode`` and ``size``.

        Raises:
            ValueError: If ``size`` has an invalid type or dimensionality.
        """
        if size is not None and not isinstance(size, int) and not isinstance(size, tuple):
            raise ValueError("Size must be None, an integer, or a tuple.")
        if size is None:
            n_samples = 1
        elif isinstance(size, int):
            n_samples = size
        elif isinstance(size, tuple):
            if len(size) > 1 and not self.array_mode:
                raise ValueError("Multiple dimensions are only supported for array_mode.")
            if len(size) == 1:
                n_samples = size[0]
            else:
                n_samples = int(np.prod(size))

        samples = []
        for _ in range(n_samples):
            if self.constraints:
                sample = self._sample_once_constrained()
            else:
                sample = self._sample_once()
            # add any parameters that are not sampled but are required for the model
            sample = ParameterSet(
                {
                    **sample,
                    **{
                        key: param.value
                        for key, param in self.parameters.items()
                        if isinstance(
                            param,
                            (IndependentScalarParameter, IndependentVectorParameter),
                        )
                        and not param.is_sampled
                    },
                }
            )
            samples.append(sample)
        if self.array_mode:
            if size is None:
                out = self.instance_to_array(samples[0])
            elif isinstance(size, int):
                array_dim = len(self.lower_bounds)  # Accounts for vector parameters
                out = np.array([self.instance_to_array(sample) for sample in samples]).reshape(
                    (size, array_dim)
                )
            elif isinstance(size, tuple):
                array_dim = len(self.lower_bounds)  # Accounts for vector parameters
                out = np.array([self.instance_to_array(sample) for sample in samples]).reshape(
                    size + (array_dim,)
                )
        else:
            if size is None:
                out = samples[0]
            elif isinstance(size, int):
                out = self.instances_to_dataframe(list(samples))
            elif isinstance(size, tuple):
                out = self.instances_to_dataframe(list(samples))
        return out

    def instances_to_dataframe(self, instances: list[ParameterSet]) -> pd.DataFrame:
        """Convert a list of parameter instances to a pandas DataFrame.

        Args:
            instances: A non-empty list of ``ParameterSet`` objects.

        Returns:
            pandas.DataFrame: Rows correspond to instances; columns to
            parameters in canonical order.

        Raises:
            ValueError: If the input is not a non-empty list of ``ParameterSet``
                objects.
        """
        if not isinstance(instances, list):
            raise ValueError("Instances must be a list of ParameterSetInstance objects.")
        if not instances:
            raise ValueError("Instances list cannot be empty.")
        if not all(isinstance(instance, ParameterSet) for instance in instances):
            raise ValueError("All items in instances must be ParameterSetInstance objects.")
        df = pd.DataFrame([dict(instance) for instance in instances])
        # Don't convert to float if we have vector parameters (arrays)
        # Only convert scalar columns to float
        for col in df.columns:
            if col in self.vector_names:
                # Keep as object dtype for vector parameters
                continue
            else:
                df[col] = df[col].astype(float)
        df = df[self.names]  # reorder columns to canonical order
        return df

    def log_prob(self, input: ParameterSet | pd.DataFrame | np.ndarray) -> float | np.ndarray:
        """Compute log-probability for parameter instances.

        Args:
            input: A ``ParameterSet``, a pandas ``DataFrame`` (rows are
                instances), or a NumPy array (arrays, the
                expected width depends on ``theta_sampling``.

        Returns:
            float | numpy.ndarray: A scalar for a single ``ParameterSet`` or a
            NumPy array of log-probabilities for batches.

        Raises:
            ValueError: If the type/shape of ``input`` is inconsistent with the
                current ``array_mode`` mode.
        """
        # categorize inputs
        if isinstance(input, ParameterSet):  # if a single sample, package it in a list
            samples = [input]
        elif isinstance(
            input, pd.DataFrame
        ):  # if a DataFrame, convert to list of ParameterSet instances
            samples = [ParameterSet(row) for _, row in input.iterrows()]
        elif isinstance(input, np.ndarray):  # if numpy array ...
            if input.ndim == 1:  # if 1D, treat as a single sample
                if (
                    input.shape[0] != len(self.sampled) and self.array_mode
                ):  # if array_mode is enabled, sample must match sampled parameters
                    raise ValueError(
                        f"1D numpy array must have length {len(self.sampled)} to match sampled parameters, since array_mode is enabled."
                    )
                elif (
                    input.shape[0] != len(self.parameters) and not self.array_mode
                ):  # if array_mode is disabled, sample must match all parameters
                    raise ValueError(
                        f"1D numpy array must have length {len(self.parameters)} to match all parameters, since array_mode is disabled."
                    )
                samples = [self.array_to_instance(input)]
            elif input.ndim == 2:  # if 2D, treat each row as a sample
                if input.shape[1] != len(self.sampled) and self.array_mode:
                    raise ValueError(
                        f"2D numpy array must have {len(self.sampled)} columns to match sampled parameters, since array_mode is enabled."
                    )
                elif input.shape[1] != len(self.parameters) and not self.array_mode:
                    raise ValueError(
                        f"2D numpy array must have {len(self.parameters)} columns to match all parameters, since array_mode is disabled."
                    )
                samples = [self.array_to_instance(row) for row in input]
            else:
                raise ValueError("Samples must be a 1D or 2D numpy array.")
        elif not isinstance(input, list):
            raise ValueError("Samples must be a list of ParameterSet instances or a numpy array.")

        results = np.zeros(len(samples))
        for i, sample in enumerate(samples):
            results[i] = self._log_prob_single(sample)
        if len(results) == 1 and isinstance(input, ParameterSet):
            return results[0]
        else:
            return results

    def _log_prob_single(self, sample: ParameterSet) -> float:
        """Log prior for a single instance under uniform bounds.

        Returns 0.0 if within bounds and satisfying constraints, otherwise
        ``-inf``.
        """
        if sample is None or not isinstance(sample, ParameterSet):
            raise ValueError("Sample must be an instance of ParameterSet.")
        result = 0.0
        for key, param in self.parameters.items():
            if isinstance(param, IndependentScalarParameter) and param.is_sampled:
                assert param.range is not None  # Type guard for mypy
                if not (param.range[0] <= sample[key] <= param.range[1]):
                    result = -np.inf
        if not all(sample.satisfies(c) for c in self.constraints):
            result = -np.inf
        return result

    def order(self, instance: ParameterSet) -> ParameterSet:
        """Reindex an instance to the bank's canonical parameter order.

        Args:
            instance: The ``ParameterSet`` to reindex.

        Returns:
            ParameterSet: A new instance with parameters ordered canonically.

        Raises:
            ValueError: If reindexing fails (e.g., missing keys).
        """
        if not isinstance(instance, ParameterSet):
            raise ValueError("Input must be an instance of ParameterSet.")
        try:
            out = instance.reindex(self.names)
        except Exception as e:
            raise ValueError("Error reordering parameters: " + str(e)) from e
        return out

    def pretty_print(self) -> None:
        """Print a human-readable summary of the bank configuration."""
        print("ParameterBank:")
        print("----------------")
        for name, param in self.parameters.items():
            print(f"{name}: {param}")
        print("Constraints:")
        print("----------------")
        for constraint in self.constraints:
            print(constraint)

names: list[str] property

Get the names of all parameters in the bank. This also defines the canonical order of the parameters.

sampled: list[str] property

Get a list of all parameters that are set to be sampled.

vector_names: list[str] property

Get a list of all vector parameter names.

lower_bounds: np.ndarray property

Get the lower bounds of all sampled parameters.

For scalar parameters, returns the scalar lower bound. For vector parameters, returns the lower bound array or scalar if uniform.

upper_bounds: np.ndarray property

Get the upper bounds of all sampled parameters.

For scalar parameters, returns the scalar upper bound. For vector parameters, returns the upper bound array or scalar if uniform.

sampled_texnames: list[str] property

Get the TeX names of all sampled parameters.

__contains__(key: str) -> bool

Check if a parameter exists in the bank.

Source code in jscip/parameter_bank.py
182
183
184
def __contains__(self, key: str) -> bool:
    """Check if a parameter exists in the bank."""
    return key in self.parameters

__len__() -> int

Get the number of parameters in the bank.

Source code in jscip/parameter_bank.py
186
187
188
def __len__(self) -> int:
    """Get the number of parameters in the bank."""
    return len(self.parameters)

__iter__() -> Iterator[str]

Iterate over the parameter names in the bank.

Source code in jscip/parameter_bank.py
190
191
192
def __iter__(self) -> Iterator[str]:
    """Iterate over the parameter names in the bank."""
    return iter(self.parameters)

__getitem__(key: str) -> IndependentScalarParameter | IndependentVectorParameter | DerivedScalarParameter

Get a parameter by its name.

Source code in jscip/parameter_bank.py
194
195
196
197
198
199
200
201
def __getitem__(
    self, key: str
) -> IndependentScalarParameter | IndependentVectorParameter | DerivedScalarParameter:
    """Get a parameter by its name."""
    if key in self.parameters:
        return self.parameters[key]
    else:
        raise KeyError(f"Parameter '{key}' not found in the bank.")

copy() -> ParameterBank

Create a copy of the ParameterBank.

Source code in jscip/parameter_bank.py
203
204
205
206
207
208
209
210
211
212
213
def copy(self) -> ParameterBank:
    """Create a copy of the ParameterBank."""
    result = ParameterBank(
        parameters={k: v.copy() for k, v in self.parameters.items()},
        constraints=self.constraints.copy(),
        array_mode=self.array_mode,
        texnames=self.texnames.copy(),
        max_attempts=self._max_attempts,
    )
    logger.debug("Copied ParameterBank: %s", result)
    return result

merge(other: ParameterBank) -> None

Merge another ParameterBank into this one. If a parameter with the same name exists, it will be overwritten.

Source code in jscip/parameter_bank.py
227
228
229
230
231
232
233
234
235
236
237
def merge(self, other: ParameterBank) -> None:
    """Merge another ParameterBank into this one.
    If a parameter with the same name exists, it will be overwritten.
    """
    if not isinstance(other, ParameterBank):
        raise ValueError("Other must be an instance of ParameterBank.")
    for key, value in other.parameters.items():
        self.parameters[key] = value.copy()
    self.constraints.extend(other.constraints)
    self._refresh_sampled_indices()
    logger.debug("Merged ParameterBank: %s", self)

add_parameter(name: str, parameter: IndependentScalarParameter | IndependentVectorParameter | DerivedScalarParameter | DerivedVectorParameter) -> None

Add a new parameter to the bank.

Source code in jscip/parameter_bank.py
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
def add_parameter(
    self,
    name: str,
    parameter: (
        IndependentScalarParameter
        | IndependentVectorParameter
        | DerivedScalarParameter
        | DerivedVectorParameter
    ),
) -> None:
    """Add a new parameter to the bank."""
    if not isinstance(
        parameter,
        (
            IndependentScalarParameter,
            IndependentVectorParameter,
            DerivedScalarParameter,
        ),
    ):
        raise ValueError(
            "Parameter must be an instance of IndependentScalarParameter, "
            "IndependentVectorParameter, or DerivedScalarParameter."
        )
    if name in self.parameters:
        raise KeyError(f"Parameter '{name}' already exists in the bank.")
    self.parameters[name] = parameter
    self._refresh_sampled_indices()
    logger.debug("Added parameter '%s' to ParameterBank: %s", name, self)

add_constraint(constraint: Callable[[ParameterSet], bool]) -> None

Add a new constraint to the bank.

Source code in jscip/parameter_bank.py
268
269
270
271
272
273
def add_constraint(self, constraint: Callable[[ParameterSet], bool]) -> None:
    """Add a new constraint to the bank."""
    if not callable(constraint):
        raise ValueError("Constraint must be a callable function.")
    self.constraints.append(constraint)
    logger.debug("Added constraint '%s' to ParameterBank: %s", constraint, self)

get_constraints() -> list[Callable[[ParameterSet], bool]]

Get all constraints in the bank.

Source code in jscip/parameter_bank.py
275
276
277
def get_constraints(self) -> list[Callable[[ParameterSet], bool]]:
    """Get all constraints in the bank."""
    return self.constraints

get_default_values(return_array: bool | None = None) -> ParameterSet | np.ndarray

Return default values for all parameters.

Computes a ParameterSet by taking the current value for all independent parameters and computing all derived parameters from those values. Optionally, returns the sampled subset as a NumPy array when return_array=True.

Parameters:

Name Type Description Default
return_array bool | None

If True, return a 1D NumPy array of sampled parameter values in canonical sampled order. If False, return a full ParameterSet. Defaults to self.array_mode.

None

Returns:

Type Description
ParameterSet | ndarray

ParameterSet | numpy.ndarray: The default instance or the sampled

ParameterSet | ndarray

values array.

Raises:

Type Description
ValueError

If return_array is not a boolean.

Source code in jscip/parameter_bank.py
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
def get_default_values(self, return_array: bool | None = None) -> ParameterSet | np.ndarray:
    """Return default values for all parameters.

    Computes a ``ParameterSet`` by taking the current ``value`` for all
    independent parameters and computing all derived parameters from those
    values. Optionally, returns the sampled subset as a NumPy array when
    ``return_array=True``.

    Args:
        return_array: If True, return a 1D NumPy array of sampled parameter
            values in canonical sampled order. If False, return a full
            ``ParameterSet``. Defaults to ``self.array_mode``.

    Returns:
        ParameterSet | numpy.ndarray: The default instance or the sampled
        values array.

    Raises:
        ValueError: If ``return_array`` is not a boolean.
    """
    if return_array is None:
        return_array = self.array_mode
    if not isinstance(return_array, bool):
        raise ValueError("return_array must be a boolean value.")
    p = ParameterSet(
        {
            key: param.value
            for key, param in self.parameters.items()
            if isinstance(param, (IndependentScalarParameter, IndependentVectorParameter))
        }
    )
    logger.debug(
        "[get_default_values] Default values for all independent parameters in the bank: %s",
        p,
    )
    p = ParameterSet(
        {
            **p,
            **{
                key: param.compute(p)
                for key, param in self.parameters.items()
                if isinstance(param, DerivedScalarParameter)
            },
        }
    )
    p = self.order(p)
    logger.debug("[get_default_values] Default values for all parameters in the bank: %s", p)
    if return_array:
        return self.instance_to_array(p)
    else:
        return p

instance_to_array(input: ParameterSet | list[ParameterSet]) -> np.ndarray

Convert a parameter instance (or list) to a sampled parameter array.

Parameters:

Name Type Description Default
input ParameterSet | list[ParameterSet]

A single ParameterSet or list of ParameterSet instances.

required

Returns:

Type Description
ndarray

numpy.ndarray: 1D array for a single instance or 2D array for a

ndarray

list of instances, containing values for sampled parameters only,

ndarray

in canonical sampled order. Vector parameters are flattened into

ndarray

the array.

Raises:

Type Description
ValueError

If input is not a ParameterSet or list thereof.

Source code in jscip/parameter_bank.py
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
def instance_to_array(self, input: ParameterSet | list[ParameterSet]) -> np.ndarray:
    """Convert a parameter instance (or list) to a sampled parameter array.

    Args:
        input: A single ``ParameterSet`` or list of ``ParameterSet``
            instances.

    Returns:
        numpy.ndarray: 1D array for a single instance or 2D array for a
        list of instances, containing values for sampled parameters only,
        in canonical sampled order. Vector parameters are flattened into
        the array.

    Raises:
        ValueError: If ``input`` is not a ``ParameterSet`` or list thereof.
    """
    if not isinstance(input, (ParameterSet, list)):
        raise ValueError(
            "Input must be a ParameterSetInstance or a list of ParameterSetInstances."
        )
    if isinstance(input, ParameterSet):
        # Flatten vector parameters into the theta array
        theta_values: list[float] = []
        for key in self.sampled:
            value = input[key]
            if isinstance(value, np.ndarray):
                theta_values.extend(value)
            else:
                theta_values.append(value)
        theta = np.array(theta_values)
        logger.debug("[instance_to_array] Converted ParameterSet to numpy array: %s", theta)
    else:
        # return a 2D array of shape (n_instances, n_theta_dims)
        theta_list = []
        for instance in input:
            theta_values = []
            for key in self.sampled:
                value = instance[key]
                if isinstance(value, np.ndarray):
                    theta_values.extend(value)
                else:
                    theta_values.append(value)
            theta_list.append(theta_values)
        theta = np.array(theta_list)
        logger.debug(
            "[instance_to_array] Converted list of ParameterSetInstances to numpy array: %s",
            theta,
        )
    return theta

dataframe_to_array(df: pd.DataFrame) -> np.ndarray

Extract sampled parameter columns from a DataFrame as a NumPy array.

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing sampled parameter columns.

required

Returns:

Type Description
ndarray

numpy.ndarray: 2D array of sampled values in canonical sampled

ndarray

order.

Raises:

Type Description
ValueError

If df is not a pandas DataFrame.

Source code in jscip/parameter_bank.py
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
def dataframe_to_array(self, df: pd.DataFrame) -> np.ndarray:
    """Extract sampled parameter columns from a DataFrame as a NumPy array.

    Args:
        df: DataFrame containing sampled parameter columns.

    Returns:
        numpy.ndarray: 2D array of sampled values in canonical sampled
        order.

    Raises:
        ValueError: If ``df`` is not a pandas DataFrame.
    """
    if not isinstance(df, pd.DataFrame):
        raise ValueError("Input must be a pandas DataFrame.")
    theta = df[self.sampled].to_numpy()
    return theta

array_to_instance(theta: np.ndarray) -> ParameterSet

Convert a parameter array to a parameter instance.

When array_mode is True, theta must contain only sampled independent parameters in canonical sampled order. Otherwise, it must contain values for all independent parameters in canonical order.

Parameters:

Name Type Description Default
theta ndarray

1D NumPy array.

required

Returns:

Name Type Description
ParameterSet ParameterSet

A full instance with derived parameters recomputed.

Raises:

Type Description
ValueError

If shapes are inconsistent with array_mode or if theta is not a NumPy array.

Source code in jscip/parameter_bank.py
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
def array_to_instance(self, theta: np.ndarray) -> ParameterSet:
    """Convert a parameter array to a parameter instance.

    When ``array_mode`` is True, ``theta`` must contain only sampled
    independent parameters in canonical sampled order. Otherwise, it must
    contain values for all independent parameters in canonical order.

    Args:
        theta: 1D NumPy array.

    Returns:
        ParameterSet: A full instance with derived parameters recomputed.

    Raises:
        ValueError: If shapes are inconsistent with ``array_mode`` or
            if ``theta`` is not a NumPy array.
    """
    if not isinstance(theta, np.ndarray):
        raise ValueError("Input must be a numpy array, instead got: " + str(type(theta)))
    # validate length depending on array_mode
    if self.array_mode:
        # Calculate expected theta length (accounting for vector parameters)
        expected_len = len(self.lower_bounds)  # This already accounts for vectors
        if len(theta) != expected_len:
            raise ValueError(
                f"Array length {len(theta)} does not match expected theta dimensions {expected_len}."
            )
    else:
        if len(theta) != len(self.parameters):
            raise ValueError(
                f"Array length {len(theta)} does not match number of parameters {len(self.parameters)}."
            )
    # theta in this case must be a 1D array
    # Start with defaults
    out = self.get_default_values(return_array=False)
    if self.array_mode:
        # theta provides only sampled independent parameters
        # Need to unflatten vector parameters
        theta_idx = 0
        for key in self.sampled:
            param = self.parameters[key]
            if isinstance(param, IndependentVectorParameter):
                # Extract vector elements from theta
                n_elements = param.shape[0]
                out[key] = theta[theta_idx : theta_idx + n_elements]
                theta_idx += n_elements
            else:
                # Scalar parameter
                out[key] = theta[theta_idx]
                theta_idx += 1
    else:
        # theta provides values for ALL parameters in canonical order
        if len(theta) != len(self.parameters):
            raise ValueError(
                f"Array length {len(theta)} does not match number of parameters {len(self.parameters)}."
            )
        for i, key in enumerate(self.names):
            param = self.parameters[key]
            if isinstance(param, IndependentScalarParameter):
                out[key] = float(theta[i])
    # recompute derived parameters
    assert isinstance(out, ParameterSet)  # Type guard for mypy
    out = ParameterSet(
        {
            **out,
            **{
                key: param.compute(out)
                for key, param in self.parameters.items()
                if isinstance(param, DerivedScalarParameter)
            },
        }
    )
    return out

sample(size: int | tuple | None = None) -> ParameterSet | pd.DataFrame | np.ndarray

Sample parameter sets or theta arrays.

Parameters:

Name Type Description Default
size int | tuple | None

If None, returns a single instance. If int, returns a batch. If tuple, returns product size; multi-d shapes are only supported when array_mode is True.

None

Returns:

Type Description
ParameterSet | DataFrame | ndarray

ParameterSet | pandas.DataFrame | numpy.ndarray: Depending on

ParameterSet | DataFrame | ndarray

array_mode and size.

Raises:

Type Description
ValueError

If size has an invalid type or dimensionality.

Source code in jscip/parameter_bank.py
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
def sample(self, size: int | tuple | None = None) -> ParameterSet | pd.DataFrame | np.ndarray:
    """Sample parameter sets or theta arrays.

    Args:
        size: If ``None``, returns a single instance. If ``int``, returns a
            batch. If ``tuple``, returns product size; multi-d shapes are
            only supported when ``array_mode`` is True.

    Returns:
        ParameterSet | pandas.DataFrame | numpy.ndarray: Depending on
        ``array_mode`` and ``size``.

    Raises:
        ValueError: If ``size`` has an invalid type or dimensionality.
    """
    if size is not None and not isinstance(size, int) and not isinstance(size, tuple):
        raise ValueError("Size must be None, an integer, or a tuple.")
    if size is None:
        n_samples = 1
    elif isinstance(size, int):
        n_samples = size
    elif isinstance(size, tuple):
        if len(size) > 1 and not self.array_mode:
            raise ValueError("Multiple dimensions are only supported for array_mode.")
        if len(size) == 1:
            n_samples = size[0]
        else:
            n_samples = int(np.prod(size))

    samples = []
    for _ in range(n_samples):
        if self.constraints:
            sample = self._sample_once_constrained()
        else:
            sample = self._sample_once()
        # add any parameters that are not sampled but are required for the model
        sample = ParameterSet(
            {
                **sample,
                **{
                    key: param.value
                    for key, param in self.parameters.items()
                    if isinstance(
                        param,
                        (IndependentScalarParameter, IndependentVectorParameter),
                    )
                    and not param.is_sampled
                },
            }
        )
        samples.append(sample)
    if self.array_mode:
        if size is None:
            out = self.instance_to_array(samples[0])
        elif isinstance(size, int):
            array_dim = len(self.lower_bounds)  # Accounts for vector parameters
            out = np.array([self.instance_to_array(sample) for sample in samples]).reshape(
                (size, array_dim)
            )
        elif isinstance(size, tuple):
            array_dim = len(self.lower_bounds)  # Accounts for vector parameters
            out = np.array([self.instance_to_array(sample) for sample in samples]).reshape(
                size + (array_dim,)
            )
    else:
        if size is None:
            out = samples[0]
        elif isinstance(size, int):
            out = self.instances_to_dataframe(list(samples))
        elif isinstance(size, tuple):
            out = self.instances_to_dataframe(list(samples))
    return out

instances_to_dataframe(instances: list[ParameterSet]) -> pd.DataFrame

Convert a list of parameter instances to a pandas DataFrame.

Parameters:

Name Type Description Default
instances list[ParameterSet]

A non-empty list of ParameterSet objects.

required

Returns:

Type Description
DataFrame

pandas.DataFrame: Rows correspond to instances; columns to

DataFrame

parameters in canonical order.

Raises:

Type Description
ValueError

If the input is not a non-empty list of ParameterSet objects.

Source code in jscip/parameter_bank.py
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
def instances_to_dataframe(self, instances: list[ParameterSet]) -> pd.DataFrame:
    """Convert a list of parameter instances to a pandas DataFrame.

    Args:
        instances: A non-empty list of ``ParameterSet`` objects.

    Returns:
        pandas.DataFrame: Rows correspond to instances; columns to
        parameters in canonical order.

    Raises:
        ValueError: If the input is not a non-empty list of ``ParameterSet``
            objects.
    """
    if not isinstance(instances, list):
        raise ValueError("Instances must be a list of ParameterSetInstance objects.")
    if not instances:
        raise ValueError("Instances list cannot be empty.")
    if not all(isinstance(instance, ParameterSet) for instance in instances):
        raise ValueError("All items in instances must be ParameterSetInstance objects.")
    df = pd.DataFrame([dict(instance) for instance in instances])
    # Don't convert to float if we have vector parameters (arrays)
    # Only convert scalar columns to float
    for col in df.columns:
        if col in self.vector_names:
            # Keep as object dtype for vector parameters
            continue
        else:
            df[col] = df[col].astype(float)
    df = df[self.names]  # reorder columns to canonical order
    return df

log_prob(input: ParameterSet | pd.DataFrame | np.ndarray) -> float | np.ndarray

Compute log-probability for parameter instances.

Parameters:

Name Type Description Default
input ParameterSet | DataFrame | ndarray

A ParameterSet, a pandas DataFrame (rows are instances), or a NumPy array (arrays, the expected width depends on theta_sampling.

required

Returns:

Type Description
float | ndarray

float | numpy.ndarray: A scalar for a single ParameterSet or a

float | ndarray

NumPy array of log-probabilities for batches.

Raises:

Type Description
ValueError

If the type/shape of input is inconsistent with the current array_mode mode.

Source code in jscip/parameter_bank.py
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
def log_prob(self, input: ParameterSet | pd.DataFrame | np.ndarray) -> float | np.ndarray:
    """Compute log-probability for parameter instances.

    Args:
        input: A ``ParameterSet``, a pandas ``DataFrame`` (rows are
            instances), or a NumPy array (arrays, the
            expected width depends on ``theta_sampling``.

    Returns:
        float | numpy.ndarray: A scalar for a single ``ParameterSet`` or a
        NumPy array of log-probabilities for batches.

    Raises:
        ValueError: If the type/shape of ``input`` is inconsistent with the
            current ``array_mode`` mode.
    """
    # categorize inputs
    if isinstance(input, ParameterSet):  # if a single sample, package it in a list
        samples = [input]
    elif isinstance(
        input, pd.DataFrame
    ):  # if a DataFrame, convert to list of ParameterSet instances
        samples = [ParameterSet(row) for _, row in input.iterrows()]
    elif isinstance(input, np.ndarray):  # if numpy array ...
        if input.ndim == 1:  # if 1D, treat as a single sample
            if (
                input.shape[0] != len(self.sampled) and self.array_mode
            ):  # if array_mode is enabled, sample must match sampled parameters
                raise ValueError(
                    f"1D numpy array must have length {len(self.sampled)} to match sampled parameters, since array_mode is enabled."
                )
            elif (
                input.shape[0] != len(self.parameters) and not self.array_mode
            ):  # if array_mode is disabled, sample must match all parameters
                raise ValueError(
                    f"1D numpy array must have length {len(self.parameters)} to match all parameters, since array_mode is disabled."
                )
            samples = [self.array_to_instance(input)]
        elif input.ndim == 2:  # if 2D, treat each row as a sample
            if input.shape[1] != len(self.sampled) and self.array_mode:
                raise ValueError(
                    f"2D numpy array must have {len(self.sampled)} columns to match sampled parameters, since array_mode is enabled."
                )
            elif input.shape[1] != len(self.parameters) and not self.array_mode:
                raise ValueError(
                    f"2D numpy array must have {len(self.parameters)} columns to match all parameters, since array_mode is disabled."
                )
            samples = [self.array_to_instance(row) for row in input]
        else:
            raise ValueError("Samples must be a 1D or 2D numpy array.")
    elif not isinstance(input, list):
        raise ValueError("Samples must be a list of ParameterSet instances or a numpy array.")

    results = np.zeros(len(samples))
    for i, sample in enumerate(samples):
        results[i] = self._log_prob_single(sample)
    if len(results) == 1 and isinstance(input, ParameterSet):
        return results[0]
    else:
        return results

order(instance: ParameterSet) -> ParameterSet

Reindex an instance to the bank's canonical parameter order.

Parameters:

Name Type Description Default
instance ParameterSet

The ParameterSet to reindex.

required

Returns:

Name Type Description
ParameterSet ParameterSet

A new instance with parameters ordered canonically.

Raises:

Type Description
ValueError

If reindexing fails (e.g., missing keys).

Source code in jscip/parameter_bank.py
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
def order(self, instance: ParameterSet) -> ParameterSet:
    """Reindex an instance to the bank's canonical parameter order.

    Args:
        instance: The ``ParameterSet`` to reindex.

    Returns:
        ParameterSet: A new instance with parameters ordered canonically.

    Raises:
        ValueError: If reindexing fails (e.g., missing keys).
    """
    if not isinstance(instance, ParameterSet):
        raise ValueError("Input must be an instance of ParameterSet.")
    try:
        out = instance.reindex(self.names)
    except Exception as e:
        raise ValueError("Error reordering parameters: " + str(e)) from e
    return out

pretty_print() -> None

Print a human-readable summary of the bank configuration.

Source code in jscip/parameter_bank.py
717
718
719
720
721
722
723
724
725
726
def pretty_print(self) -> None:
    """Print a human-readable summary of the bank configuration."""
    print("ParameterBank:")
    print("----------------")
    for name, param in self.parameters.items():
        print(f"{name}: {param}")
    print("Constraints:")
    print("----------------")
    for constraint in self.constraints:
        print(constraint)

ParameterSet

A single parameter configuration with scalar and/or vector values.

This is a thin wrapper around pandas.Series used to represent a single instance of parameters, typically produced by sampling a ParameterBank. It can store both scalar values (from IndependentScalarParameter or DerivedScalarParameter) and vector values (from IndependentVectorParameter as numpy arrays). It preserves the canonical parameter ordering maintained by the bank when reindexed via ParameterBank.order.

Source code in jscip/parameter_set.py
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
class ParameterSet(pd.Series):
    """A single parameter configuration with scalar and/or vector values.

    This is a thin wrapper around ``pandas.Series`` used to represent a single
    instance of parameters, typically produced by sampling a ``ParameterBank``.
    It can store both scalar values (from ``IndependentScalarParameter`` or
    ``DerivedScalarParameter``) and vector values (from ``IndependentVectorParameter``
    as numpy arrays). It preserves the canonical parameter ordering maintained
    by the bank when reindexed via ``ParameterBank.order``.
    """

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

    def __repr__(self) -> str:
        return f"ParameterSet({super().__repr__()})"

    def satisfies(self, constraint: Callable[[ParameterSet], bool]) -> bool:
        """Evaluate a boolean constraint on this instance.

        Args:
            constraint: A callable ``f(ps: ParameterSet) -> bool``.

        Returns:
            bool: True if the constraint is satisfied, otherwise False.

        Raises:
            ValueError: If ``constraint`` is not callable or does not return a
                boolean-like value.
        """
        if not callable(constraint):
            raise ValueError("Constraint must be a callable function.")
        result = constraint(self)
        if not isinstance(result, (bool, np.bool_)):
            raise ValueError("Constraint function must return a boolean value.")
        return result

    def copy(self) -> ParameterSet:
        """Return a copy of this parameter set.

        Returns:
            ParameterSet: A new instance with the same values.

        Note:
            Numpy arrays are deep copied to prevent unintended mutations.
        """
        # Deep copy any numpy arrays to prevent shared references
        data = {}
        for key, value in self.items():
            if isinstance(value, np.ndarray):
                data[key] = value.copy()
            else:
                data[key] = value
        result = ParameterSet(data)
        logger.debug("Copied ParameterSet: %s", result)
        return result

    def reindex(self, new_index: Sequence[str]) -> ParameterSet:
        """Reindex this instance to a new sequence of parameter names.

        Args:
            new_index: Iterable of parameter names specifying the new order.

        Returns:
            ParameterSet: A new instance with the requested index.

        Raises:
            ValueError: If ``new_index`` is not a list or tuple.
        """
        if not isinstance(new_index, (list, tuple)):
            raise ValueError("New index must be a list or tuple of parameter names.")
        new_series = super().reindex(new_index)
        result = ParameterSet(new_series)
        logger.debug("Reindexed ParameterSet: %s", result)
        return result

satisfies(constraint: Callable[[ParameterSet], bool]) -> bool

Evaluate a boolean constraint on this instance.

Parameters:

Name Type Description Default
constraint Callable[[ParameterSet], bool]

A callable f(ps: ParameterSet) -> bool.

required

Returns:

Name Type Description
bool bool

True if the constraint is satisfied, otherwise False.

Raises:

Type Description
ValueError

If constraint is not callable or does not return a boolean-like value.

Source code in jscip/parameter_set.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def satisfies(self, constraint: Callable[[ParameterSet], bool]) -> bool:
    """Evaluate a boolean constraint on this instance.

    Args:
        constraint: A callable ``f(ps: ParameterSet) -> bool``.

    Returns:
        bool: True if the constraint is satisfied, otherwise False.

    Raises:
        ValueError: If ``constraint`` is not callable or does not return a
            boolean-like value.
    """
    if not callable(constraint):
        raise ValueError("Constraint must be a callable function.")
    result = constraint(self)
    if not isinstance(result, (bool, np.bool_)):
        raise ValueError("Constraint function must return a boolean value.")
    return result

copy() -> ParameterSet

Return a copy of this parameter set.

Returns:

Name Type Description
ParameterSet ParameterSet

A new instance with the same values.

Note

Numpy arrays are deep copied to prevent unintended mutations.

Source code in jscip/parameter_set.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def copy(self) -> ParameterSet:
    """Return a copy of this parameter set.

    Returns:
        ParameterSet: A new instance with the same values.

    Note:
        Numpy arrays are deep copied to prevent unintended mutations.
    """
    # Deep copy any numpy arrays to prevent shared references
    data = {}
    for key, value in self.items():
        if isinstance(value, np.ndarray):
            data[key] = value.copy()
        else:
            data[key] = value
    result = ParameterSet(data)
    logger.debug("Copied ParameterSet: %s", result)
    return result

reindex(new_index: Sequence[str]) -> ParameterSet

Reindex this instance to a new sequence of parameter names.

Parameters:

Name Type Description Default
new_index Sequence[str]

Iterable of parameter names specifying the new order.

required

Returns:

Name Type Description
ParameterSet ParameterSet

A new instance with the requested index.

Raises:

Type Description
ValueError

If new_index is not a list or tuple.

Source code in jscip/parameter_set.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def reindex(self, new_index: Sequence[str]) -> ParameterSet:
    """Reindex this instance to a new sequence of parameter names.

    Args:
        new_index: Iterable of parameter names specifying the new order.

    Returns:
        ParameterSet: A new instance with the requested index.

    Raises:
        ValueError: If ``new_index`` is not a list or tuple.
    """
    if not isinstance(new_index, (list, tuple)):
        raise ValueError("New index must be a list or tuple of parameter names.")
    new_series = super().reindex(new_index)
    result = ParameterSet(new_series)
    logger.debug("Reindexed ParameterSet: %s", result)
    return result

DerivedParameter

Base class for derived parameters (scalar or vector).

Derived parameters are computed from other parameters via a function. They are never sampled directly.

Attributes:

Name Type Description
function

Callable that computes the derived value from a ParameterSet.

is_sampled bool

Always False for derived parameters.

Source code in jscip/parameters.py
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
class DerivedParameter:
    """Base class for derived parameters (scalar or vector).

    Derived parameters are computed from other parameters via a function.
    They are never sampled directly.

    Attributes:
        function: Callable that computes the derived value from a ParameterSet.
        is_sampled: Always False for derived parameters.
    """

    def __init__(self, function):
        if not callable(function):
            raise ValueError("Function must be callable.")
        self.function = function
        self._is_sampled = False

    @property
    def is_sampled(self) -> bool:
        """Get whether this parameter is sampled (always False)."""
        return self._is_sampled

    def compute(self, parameters):
        """Compute the derived value for a given parameter set.

        Must be implemented by subclasses.
        """
        raise NotImplementedError("Subclasses must implement compute()")

    def copy(self):
        """Return a copy of this parameter.

        Must be implemented by subclasses.
        """
        raise NotImplementedError("Subclasses must implement copy()")

is_sampled: bool property

Get whether this parameter is sampled (always False).

compute(parameters)

Compute the derived value for a given parameter set.

Must be implemented by subclasses.

Source code in jscip/parameters.py
79
80
81
82
83
84
def compute(self, parameters):
    """Compute the derived value for a given parameter set.

    Must be implemented by subclasses.
    """
    raise NotImplementedError("Subclasses must implement compute()")

copy()

Return a copy of this parameter.

Must be implemented by subclasses.

Source code in jscip/parameters.py
86
87
88
89
90
91
def copy(self):
    """Return a copy of this parameter.

    Must be implemented by subclasses.
    """
    raise NotImplementedError("Subclasses must implement copy()")

DerivedScalarParameter

A read-only parameter computed from other parameters.

A DerivedScalarParameter wraps a function that maps a ParameterSet to a scalar value. It is not sampled directly and is recomputed whenever an instance is formed or updated.

Source code in jscip/parameters.py
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
class DerivedScalarParameter(DerivedParameter):
    """A read-only parameter computed from other parameters.

    A ``DerivedScalarParameter`` wraps a function that maps a ``ParameterSet`` to a
    scalar value. It is not sampled directly and is recomputed whenever an
    instance is formed or updated.
    """

    def __init__(self, function) -> None:
        super().__init__(function)
        logger.debug("Initialized DerivedScalarParameter with function %s", self.function)

    def __repr__(self) -> str:
        return f"DerivedScalarParameter(function={self.function.__name__})"

    def compute(self, parameters: ParameterSet) -> float:
        """Compute the derived scalar value for a given parameter set.

        Args:
            parameters: A ParameterSet containing the independent parameters.

        Returns:
            float: The computed scalar value.
        """
        from .parameter_set import ParameterSet

        if not isinstance(parameters, ParameterSet):
            raise ValueError("Parameters must be an instance of ParameterSet.")
        result = self.function(parameters)
        logger.debug("Computed value of DerivedScalarParameter: %s", result)
        return float(result)

    def copy(self) -> DerivedScalarParameter:
        """Return a shallow copy preserving the underlying function.

        Returns:
            DerivedScalarParameter: A new wrapper around the same function.
        """
        result = DerivedScalarParameter(function=self.function)
        logger.debug("Copied DerivedScalarParameter: %s", result)
        return result

compute(parameters: ParameterSet) -> float

Compute the derived scalar value for a given parameter set.

Parameters:

Name Type Description Default
parameters ParameterSet

A ParameterSet containing the independent parameters.

required

Returns:

Name Type Description
float float

The computed scalar value.

Source code in jscip/parameters.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def compute(self, parameters: ParameterSet) -> float:
    """Compute the derived scalar value for a given parameter set.

    Args:
        parameters: A ParameterSet containing the independent parameters.

    Returns:
        float: The computed scalar value.
    """
    from .parameter_set import ParameterSet

    if not isinstance(parameters, ParameterSet):
        raise ValueError("Parameters must be an instance of ParameterSet.")
    result = self.function(parameters)
    logger.debug("Computed value of DerivedScalarParameter: %s", result)
    return float(result)

copy() -> DerivedScalarParameter

Return a shallow copy preserving the underlying function.

Returns:

Name Type Description
DerivedScalarParameter DerivedScalarParameter

A new wrapper around the same function.

Source code in jscip/parameters.py
259
260
261
262
263
264
265
266
267
def copy(self) -> DerivedScalarParameter:
    """Return a shallow copy preserving the underlying function.

    Returns:
        DerivedScalarParameter: A new wrapper around the same function.
    """
    result = DerivedScalarParameter(function=self.function)
    logger.debug("Copied DerivedScalarParameter: %s", result)
    return result

DerivedVectorParameter

A read-only vector parameter computed from other parameters.

A DerivedVectorParameter wraps a function that maps a ParameterSet to a vector value. It is not sampled directly and is recomputed whenever an instance is formed or updated.

Attributes:

Name Type Description
function

Callable that computes the derived vector from a ParameterSet.

output_shape tuple[int, ...]

Expected shape of the output vector (e.g., (3,) for 3D vector).

is_sampled bool

Always False for derived parameters.

Source code in jscip/parameters.py
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
class DerivedVectorParameter(DerivedParameter):
    """A read-only vector parameter computed from other parameters.

    A ``DerivedVectorParameter`` wraps a function that maps a ``ParameterSet`` to a
    vector value. It is not sampled directly and is recomputed whenever an
    instance is formed or updated.

    Attributes:
        function: Callable that computes the derived vector from a ParameterSet.
        output_shape: Expected shape of the output vector (e.g., (3,) for 3D vector).
        is_sampled: Always False for derived parameters.
    """

    def __init__(
        self,
        function: Callable[[ParameterSet], np.ndarray],
        output_shape: tuple[int, ...],
    ) -> None:
        """Initialize a DerivedVectorParameter.

        Args:
            function: Callable that takes a ParameterSet and returns a numpy array.
            output_shape: Expected shape of the output (e.g., (3,) for 3D vector).

        Raises:
            ValueError: If function is not callable or output_shape is invalid.
        """
        super().__init__(function)

        if not isinstance(output_shape, tuple):
            raise ValueError("output_shape must be a tuple")
        if not all(isinstance(d, int) and d > 0 for d in output_shape):
            raise ValueError("output_shape must contain positive integers")

        self._output_shape = output_shape
        logger.debug(
            "Initialized DerivedVectorParameter with function %s, output_shape %s",
            self.function,
            output_shape,
        )

    @property
    def output_shape(self) -> tuple[int, ...]:
        """Get the expected output shape."""
        return self._output_shape

    @property
    def shape(self) -> tuple[int, ...]:
        """Get the shape of the parameter (alias for output_shape)."""
        return self._output_shape

    def __repr__(self) -> str:
        return (
            f"DerivedVectorParameter(function={self.function.__name__}, "
            f"output_shape={self.output_shape})"
        )

    def compute(self, parameters: ParameterSet) -> np.ndarray:
        """Compute the derived vector value for a given parameter set.

        Args:
            parameters: The ``ParameterSet`` providing inputs to the function.

        Returns:
            numpy.ndarray: The computed vector value.

        Raises:
            ValueError: If ``parameters`` is not a ``ParameterSet``, the
                stored function is not callable, or the output shape doesn't
                match the expected shape.
        """
        from .parameter_set import ParameterSet

        if not isinstance(parameters, ParameterSet):
            raise ValueError("Parameters must be an instance of ParameterSet.")
        if not callable(self.function):
            raise ValueError("Function must be callable.")

        result = self.function(parameters)

        # Validate output is a numpy array
        if not isinstance(result, np.ndarray):
            raise ValueError(f"Function must return a numpy array, got {type(result)}")

        # Validate output shape
        if result.shape != self._output_shape:
            raise ValueError(
                f"Function output shape {result.shape} does not match "
                f"expected shape {self._output_shape}"
            )

        logger.debug("Computed value of DerivedVectorParameter: %s", result)
        return result

    def copy(self) -> DerivedVectorParameter:
        """Return a shallow copy preserving the underlying function.

        Returns:
            DerivedVectorParameter: A new wrapper around the same function.
        """
        result = DerivedVectorParameter(function=self.function, output_shape=self._output_shape)
        logger.debug("Copied DerivedVectorParameter: %s", result)
        return result

output_shape: tuple[int, ...] property

Get the expected output shape.

shape: tuple[int, ...] property

Get the shape of the parameter (alias for output_shape).

__init__(function: Callable[[ParameterSet], np.ndarray], output_shape: tuple[int, ...]) -> None

Initialize a DerivedVectorParameter.

Parameters:

Name Type Description Default
function Callable[[ParameterSet], ndarray]

Callable that takes a ParameterSet and returns a numpy array.

required
output_shape tuple[int, ...]

Expected shape of the output (e.g., (3,) for 3D vector).

required

Raises:

Type Description
ValueError

If function is not callable or output_shape is invalid.

Source code in jscip/parameters.py
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
def __init__(
    self,
    function: Callable[[ParameterSet], np.ndarray],
    output_shape: tuple[int, ...],
) -> None:
    """Initialize a DerivedVectorParameter.

    Args:
        function: Callable that takes a ParameterSet and returns a numpy array.
        output_shape: Expected shape of the output (e.g., (3,) for 3D vector).

    Raises:
        ValueError: If function is not callable or output_shape is invalid.
    """
    super().__init__(function)

    if not isinstance(output_shape, tuple):
        raise ValueError("output_shape must be a tuple")
    if not all(isinstance(d, int) and d > 0 for d in output_shape):
        raise ValueError("output_shape must contain positive integers")

    self._output_shape = output_shape
    logger.debug(
        "Initialized DerivedVectorParameter with function %s, output_shape %s",
        self.function,
        output_shape,
    )

compute(parameters: ParameterSet) -> np.ndarray

Compute the derived vector value for a given parameter set.

Parameters:

Name Type Description Default
parameters ParameterSet

The ParameterSet providing inputs to the function.

required

Returns:

Type Description
ndarray

numpy.ndarray: The computed vector value.

Raises:

Type Description
ValueError

If parameters is not a ParameterSet, the stored function is not callable, or the output shape doesn't match the expected shape.

Source code in jscip/parameters.py
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
def compute(self, parameters: ParameterSet) -> np.ndarray:
    """Compute the derived vector value for a given parameter set.

    Args:
        parameters: The ``ParameterSet`` providing inputs to the function.

    Returns:
        numpy.ndarray: The computed vector value.

    Raises:
        ValueError: If ``parameters`` is not a ``ParameterSet``, the
            stored function is not callable, or the output shape doesn't
            match the expected shape.
    """
    from .parameter_set import ParameterSet

    if not isinstance(parameters, ParameterSet):
        raise ValueError("Parameters must be an instance of ParameterSet.")
    if not callable(self.function):
        raise ValueError("Function must be callable.")

    result = self.function(parameters)

    # Validate output is a numpy array
    if not isinstance(result, np.ndarray):
        raise ValueError(f"Function must return a numpy array, got {type(result)}")

    # Validate output shape
    if result.shape != self._output_shape:
        raise ValueError(
            f"Function output shape {result.shape} does not match "
            f"expected shape {self._output_shape}"
        )

    logger.debug("Computed value of DerivedVectorParameter: %s", result)
    return result

copy() -> DerivedVectorParameter

Return a shallow copy preserving the underlying function.

Returns:

Name Type Description
DerivedVectorParameter DerivedVectorParameter

A new wrapper around the same function.

Source code in jscip/parameters.py
364
365
366
367
368
369
370
371
372
def copy(self) -> DerivedVectorParameter:
    """Return a shallow copy preserving the underlying function.

    Returns:
        DerivedVectorParameter: A new wrapper around the same function.
    """
    result = DerivedVectorParameter(function=self.function, output_shape=self._output_shape)
    logger.debug("Copied DerivedVectorParameter: %s", result)
    return result

IndependentParameter

Base class for independent parameters (scalar or vector).

Independent parameters can be sampled from distributions or held fixed. They are the primary inputs to a parameter space.

Attributes:

Name Type Description
is_sampled bool

Whether this parameter will be sampled from a distribution.

Source code in jscip/parameters.py
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
class IndependentParameter:
    """Base class for independent parameters (scalar or vector).

    Independent parameters can be sampled from distributions or held fixed.
    They are the primary inputs to a parameter space.

    Attributes:
        is_sampled: Whether this parameter will be sampled from a distribution.
    """

    def __init__(self, is_sampled: bool = False):
        self._is_sampled = is_sampled

    @property
    def is_sampled(self) -> bool:
        """Get whether this parameter is sampled."""
        return self._is_sampled

    def sample(self, size: int | None = None):
        """Sample from the parameter's distribution.

        Must be implemented by subclasses.
        """
        raise NotImplementedError("Subclasses must implement sample()")

    def copy(self):
        """Return a copy of this parameter.

        Must be implemented by subclasses.
        """
        raise NotImplementedError("Subclasses must implement copy()")

is_sampled: bool property

Get whether this parameter is sampled.

sample(size: int | None = None)

Sample from the parameter's distribution.

Must be implemented by subclasses.

Source code in jscip/parameters.py
42
43
44
45
46
47
def sample(self, size: int | None = None):
    """Sample from the parameter's distribution.

    Must be implemented by subclasses.
    """
    raise NotImplementedError("Subclasses must implement sample()")

copy()

Return a copy of this parameter.

Must be implemented by subclasses.

Source code in jscip/parameters.py
49
50
51
52
53
54
def copy(self):
    """Return a copy of this parameter.

    Must be implemented by subclasses.
    """
    raise NotImplementedError("Subclasses must implement copy()")

IndependentScalarParameter

A real-valued parameter with optional uniform sampling over a range.

This class represents a scalar numeric parameter. When is_sampled=True, a uniform distribution over range=(low, high) is constructed to draw samples; otherwise the parameter is treated as fixed at value.

Attributes:

Name Type Description
value float

Current scalar value of the parameter.

is_sampled bool

Whether this parameter will be sampled from a distribution.

range tuple[float, float] | None

Optional inclusive bounds (low, high) for the parameter.

Raises:

Type Description
ValueError

If types are invalid, if the range is malformed, or if the value falls outside the provided range.

Source code in jscip/parameters.py
 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
class IndependentScalarParameter(IndependentParameter):
    """A real-valued parameter with optional uniform sampling over a range.

    This class represents a scalar numeric parameter. When `is_sampled=True`, a
    uniform distribution over `range=(low, high)` is constructed to draw
    samples; otherwise the parameter is treated as fixed at `value`.

    Attributes:
        value: Current scalar value of the parameter.
        is_sampled: Whether this parameter will be sampled from a distribution.
        range: Optional inclusive bounds `(low, high)` for the parameter.

    Raises:
        ValueError: If types are invalid, if the range is malformed, or if the
            value falls outside the provided range.
    """

    def __init__(
        self,
        value: float,
        is_sampled: bool = False,
        range: tuple[float, float] | None = None,
    ):
        super().__init__(is_sampled=is_sampled)
        self._validate_value_and_range(value, range)
        self._value = value
        self._range = range

        if is_sampled and (range is None or len(range) != 2):
            raise ValueError("If is_sampled is True, range must be a tuple of two numeric values.")

        if is_sampled and range is not None:
            assert self.range is not None  # Type guard for mypy
            self._distribution = stats.uniform(
                loc=self.range[0], scale=self.range[1] - self.range[0]
            )
        else:
            self._distribution = None
        logger.debug(
            "Initialized IndependentScalarParameter with value %s, range %s, is_sampled %s",
            value,
            range,
            is_sampled,
        )

    @property
    def value(self) -> float:
        """Get the value of the parameter."""
        return self._value

    @value.setter
    def value(self, value: float) -> None:
        self._validate_value_and_range(value, self.range)
        self._value = value
        logger.debug("Set value of IndependentScalarParameter to %s", value)

    @property
    def range(self) -> tuple[float, float] | None:
        """Get the range of the parameter."""
        return self._range

    @range.setter
    def range(self, range: tuple[float, float] | None) -> None:
        self._validate_value_and_range(self.value, range)
        self._range = range
        logger.debug("Set range of IndependentScalarParameter to %s", range)

    def __repr__(self) -> str:
        return f"IndependentScalarParameter(value={self.value}, range={self.range}, is_sampled={self.is_sampled})"

    def __str__(self) -> str:
        return self.__repr__()

    def _validate_value_and_range(self, value: float, range: tuple[float, float] | None) -> None:
        """Validate value and range consistency.

        Args:
            value: Candidate scalar value.
            range: Either `None` or a tuple `(low, high)` with numeric bounds.

        Raises:
            ValueError: If `value` is non-numeric, `range` is malformed, or
                `value` is not within the specified range.
        """
        if not isinstance(value, (int, float)):
            raise ValueError("Value must be a number.")
        if range is not None:
            if not isinstance(range, tuple) or len(range) != 2:
                raise ValueError("Range must be a tuple of two elements.")
            if not all(isinstance(x, (int, float)) for x in range):
                raise ValueError("Range must contain only numeric values.")
            if not (range[0] <= value <= range[1]):
                raise ValueError(f"Value {value} is not within the range {range}.")
        logger.debug(
            "Validated value and range of IndependentScalarParameter: value %s, range %s",
            value,
            range,
        )

    def sample(self, size: int | None = None) -> float:
        """Sample from the parameter's distribution.

        If `is_sampled` is True, draws from a uniform distribution over
        `range`. Otherwise, returns the fixed `value`.

        Args:
            size: Optional number of samples. If omitted, returns a scalar.

        Returns:
            float | numpy.ndarray: A single float if `size is None`, otherwise
            a NumPy array of samples.
        """
        if self.is_sampled:
            result = self._distribution.rvs(size=size)
        else:
            result = self.value
        logger.debug("Sampled value from IndependentScalarParameter: %s", result)
        return result

    def copy(self) -> IndependentScalarParameter:
        """Return a shallow copy preserving configuration.

        Returns:
            IndependentScalarParameter: A new parameter with the same value, range,
            and sampling flag.
        """
        result = IndependentScalarParameter(
            value=self.value, is_sampled=self.is_sampled, range=self.range
        )
        logger.debug("Copied IndependentScalarParameter: %s", result)
        return result

value: float property writable

Get the value of the parameter.

range: tuple[float, float] | None property writable

Get the range of the parameter.

sample(size: int | None = None) -> float

Sample from the parameter's distribution.

If is_sampled is True, draws from a uniform distribution over range. Otherwise, returns the fixed value.

Parameters:

Name Type Description Default
size int | None

Optional number of samples. If omitted, returns a scalar.

None

Returns:

Type Description
float

float | numpy.ndarray: A single float if size is None, otherwise

float

a NumPy array of samples.

Source code in jscip/parameters.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def sample(self, size: int | None = None) -> float:
    """Sample from the parameter's distribution.

    If `is_sampled` is True, draws from a uniform distribution over
    `range`. Otherwise, returns the fixed `value`.

    Args:
        size: Optional number of samples. If omitted, returns a scalar.

    Returns:
        float | numpy.ndarray: A single float if `size is None`, otherwise
        a NumPy array of samples.
    """
    if self.is_sampled:
        result = self._distribution.rvs(size=size)
    else:
        result = self.value
    logger.debug("Sampled value from IndependentScalarParameter: %s", result)
    return result

copy() -> IndependentScalarParameter

Return a shallow copy preserving configuration.

Returns:

Name Type Description
IndependentScalarParameter IndependentScalarParameter

A new parameter with the same value, range,

IndependentScalarParameter

and sampling flag.

Source code in jscip/parameters.py
213
214
215
216
217
218
219
220
221
222
223
224
def copy(self) -> IndependentScalarParameter:
    """Return a shallow copy preserving configuration.

    Returns:
        IndependentScalarParameter: A new parameter with the same value, range,
        and sampling flag.
    """
    result = IndependentScalarParameter(
        value=self.value, is_sampled=self.is_sampled, range=self.range
    )
    logger.debug("Copied IndependentScalarParameter: %s", result)
    return result

IndependentVectorParameter

A vector-valued parameter with optional multivariate sampling.

This class represents an Nx1 vector parameter that can be sampled from multivariate distributions. The parameter can be initialized with a list or NumPy array and supports element-wise or uniform range specifications.

Attributes:

Name Type Description
value ndarray

Current vector value as a 1D NumPy array of length N.

shape tuple[int]

Tuple (N,) representing the dimensionality.

is_sampled bool

Whether this parameter will be sampled from a distribution.

range tuple[ndarray, ndarray] | None

Element-wise bounds as (low_array, high_array) or None.

distribution str

Distribution type ('uniform' or 'mvnormal').

Raises:

Type Description
ValueError

If types are invalid, shapes are inconsistent, or values fall outside the provided ranges.

Source code in jscip/parameters.py
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
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
class IndependentVectorParameter(IndependentParameter):
    """A vector-valued parameter with optional multivariate sampling.

    This class represents an Nx1 vector parameter that can be sampled from
    multivariate distributions. The parameter can be initialized with a list
    or NumPy array and supports element-wise or uniform range specifications.

    Attributes:
        value: Current vector value as a 1D NumPy array of length N.
        shape: Tuple (N,) representing the dimensionality.
        is_sampled: Whether this parameter will be sampled from a distribution.
        range: Element-wise bounds as (low_array, high_array) or None.
        distribution: Distribution type ('uniform' or 'mvnormal').

    Raises:
        ValueError: If types are invalid, shapes are inconsistent, or values
            fall outside the provided ranges.
    """

    def __init__(
        self,
        value: list | np.ndarray,
        is_sampled: bool = False,
        range: (tuple[list | np.ndarray, list | np.ndarray] | tuple[float, float] | None) = None,
        distribution: Literal["uniform", "mvnormal"] = "uniform",
        cov: np.ndarray | None = None,
    ):
        """Initialize a IndependentVectorParameter.

        Args:
            value: Initial vector value as list or 1D array of length N.
            is_sampled: Whether to sample this parameter.
            range: Either:
                - tuple of (low, high) arrays/lists of length N for element-wise bounds
                - tuple of (low, high) floats to apply same range to all elements
                - None for no range constraints
            distribution: Distribution type - 'uniform' or 'mvnormal'.
            cov: Covariance matrix for 'mvnormal' distribution (NxN array).
                If None and distribution='mvnormal', uses identity matrix.

        Raises:
            ValueError: If value is not 1D, range shapes don't match, or
                distribution parameters are invalid.
        """
        # Call base class constructor
        super().__init__(is_sampled=is_sampled)

        # Convert value to numpy array and validate
        self._value = self._validate_and_convert_value(value)
        self._shape = self._value.shape

        # Validate and store range
        self._range = self._validate_and_convert_range(range, self._shape[0])

        # Validate distribution
        if distribution not in ("uniform", "mvnormal"):
            raise ValueError("distribution must be 'uniform' or 'mvnormal'")
        self._distribution = distribution

        # Validate is_sampled requirements
        if is_sampled and range is None:
            raise ValueError("If is_sampled is True, range must be provided.")

        # Set up distribution for sampling
        self._dist = None
        if is_sampled:
            if distribution == "uniform":
                # For uniform, we'll sample each element independently
                self._dist = None  # Will use element-wise uniform sampling
            elif distribution == "mvnormal":
                # Set up multivariate normal distribution
                if self._range is None:
                    raise ValueError("range must be provided for mvnormal distribution")
                # Use mean as midpoint of range
                mean = (self._range[0] + self._range[1]) / 2.0

                # Use provided covariance or identity
                if cov is None:
                    # Default: identity covariance (independent components)
                    cov_matrix = np.eye(self._shape[0])
                else:
                    cov_matrix = np.asarray(cov)
                    if cov_matrix.shape != (self._shape[0], self._shape[0]):
                        raise ValueError(
                            f"Covariance matrix must be {self._shape[0]}x{self._shape[0]}, "
                            f"got {cov_matrix.shape}"
                        )

                self._dist = stats.multivariate_normal(mean=mean, cov=cov_matrix)
                self._cov = cov_matrix
            else:
                raise ValueError(f"Unknown distribution: {distribution}")

        logger.debug(
            "Initialized IndependentVectorParameter with shape %s, is_sampled %s, distribution %s",
            self._shape,
            is_sampled,
            distribution,
        )

    def _validate_and_convert_value(self, value: list | np.ndarray) -> np.ndarray:
        """Validate and convert value to 1D numpy array.

        Args:
            value: Input value as list or array.

        Returns:
            1D numpy array.

        Raises:
            ValueError: If value is not 1D or contains non-numeric values.
        """
        arr = np.asarray(value, dtype=float)

        if arr.ndim != 1:
            raise ValueError(
                f"IndependentVectorParameter value must be 1D (Nx1), got shape {arr.shape}"
            )

        if arr.size == 0:
            raise ValueError("IndependentVectorParameter value cannot be empty")

        return arr

    def _validate_and_convert_range(
        self,
        range: tuple[list | np.ndarray, list | np.ndarray] | tuple[float, float] | None,
        n: int,
    ) -> tuple[np.ndarray, np.ndarray] | None:
        """Validate and convert range specification.

        Args:
            range: Range specification (see __init__ docstring).
            n: Length of the vector.

        Returns:
            Tuple of (low_array, high_array) or None.

        Raises:
            ValueError: If range specification is invalid.
        """
        if range is None:
            return None

        if not isinstance(range, tuple) or len(range) != 2:
            raise ValueError("range must be a tuple of (low, high)")

        low, high = range

        # Case 1: Single float tuple - apply to all elements
        if isinstance(low, (int, float)) and isinstance(high, (int, float)):
            low_arr = np.full(n, float(low))
            high_arr = np.full(n, float(high))
        else:
            # Case 2: Array/list tuple - element-wise
            low_arr = np.asarray(low, dtype=float)
            high_arr = np.asarray(high, dtype=float)

            if low_arr.shape != (n,) or high_arr.shape != (n,):
                raise ValueError(
                    f"range arrays must have shape ({n},), "
                    f"got low: {low_arr.shape}, high: {high_arr.shape}"
                )

        # Validate that low <= high for all elements
        if not np.all(low_arr <= high_arr):
            raise ValueError("All low values must be <= corresponding high values")

        # Validate that current value is within range
        if not np.all((self._value >= low_arr) & (self._value <= high_arr)):
            raise ValueError(f"Value {self._value} is not within range [{low_arr}, {high_arr}]")

        return (low_arr, high_arr)

    @property
    def value(self) -> np.ndarray:
        """Get the current value of the parameter."""
        return self._value

    @value.setter
    def value(self, value: list | np.ndarray) -> None:
        """Set the value of the parameter.

        Args:
            value: New value as list or 1D array.

        Raises:
            ValueError: If value is invalid or outside range.
        """
        new_value = self._validate_and_convert_value(value)

        if new_value.shape != self._shape:
            raise ValueError(
                f"New value shape {new_value.shape} does not match parameter shape {self._shape}"
            )

        if self._range is not None:
            low, high = self._range
            if not np.all((new_value >= low) & (new_value <= high)):
                raise ValueError(f"Value {new_value} is not within range [{low}, {high}]")

        self._value = new_value
        logger.debug("Set value of IndependentVectorParameter to %s", new_value)

    @property
    def shape(self) -> tuple[int]:
        """Get the shape of the parameter."""
        return self._shape

    @property
    def range(self) -> tuple[np.ndarray, np.ndarray] | None:
        """Get the range of the parameter."""
        return self._range

    @property
    def distribution(self) -> str:
        """Get the distribution type."""
        return self._distribution

    def __repr__(self) -> str:
        return (
            f"IndependentVectorParameter(shape={self.shape}, value={self.value}, "
            f"is_sampled={self.is_sampled}, distribution={self.distribution})"
        )

    def __str__(self) -> str:
        return self.__repr__()

    def sample(self, size: int | None = None) -> np.ndarray:
        """Sample from the parameter's distribution.

        If `is_sampled` is True, draws from the configured distribution.
        Otherwise, returns the fixed `value`.

        Args:
            size: Optional number of samples. If omitted, returns a single
                sample with shape matching the parameter shape.

        Returns:
            numpy.ndarray: If size is None, returns array of shape (N,).
                If size is provided, returns array of shape (size, N).
        """
        if not self.is_sampled:
            if size is None:
                result = self.value.copy()
            else:
                result = np.tile(self.value, (size, 1))
            logger.debug("Returned fixed value from IndependentVectorParameter: %s", result)
            return result

        if self._distribution == "uniform":
            # Sample each element independently from uniform distribution
            assert self._range is not None  # Type guard for mypy
            low, high = self._range
            if size is None:
                result = np.random.uniform(low, high)
            else:
                result = np.random.uniform(low, high, size=(size, len(low)))
        elif self._distribution == "mvnormal":
            # Sample from multivariate normal
            assert self._dist is not None  # Type guard for mypy
            assert self._range is not None  # Type guard for mypy
            if size is None:
                sample = self._dist.rvs()
                # Clip to range
                low, high = self._range
                result = np.clip(sample, low, high)
            else:
                samples = self._dist.rvs(size=size)
                # Clip to range
                low, high = self._range
                result = np.clip(samples, low, high)
        else:
            raise ValueError(f"Unknown distribution: {self._distribution}")

        logger.debug("Sampled value from IndependentVectorParameter: %s", result)
        return result

    def copy(self) -> IndependentVectorParameter:
        """Return a copy preserving configuration.

        Returns:
            IndependentVectorParameter: A new parameter with the same configuration.
        """
        # Reconstruct range in original format
        if self._range is not None:
            range_copy = (self._range[0].copy(), self._range[1].copy())
        else:
            range_copy = None

        # Get covariance if mvnormal
        cov_copy = None
        if self._distribution == "mvnormal" and hasattr(self, "_cov"):
            cov_copy = self._cov.copy()

        result = IndependentVectorParameter(
            value=self.value.copy(),
            is_sampled=self.is_sampled,
            range=range_copy,
            distribution=self._distribution,  # Use _distribution to get Literal type
            cov=cov_copy,
        )
        logger.debug("Copied IndependentVectorParameter: %s", result)
        return result

value: np.ndarray property writable

Get the current value of the parameter.

shape: tuple[int] property

Get the shape of the parameter.

range: tuple[np.ndarray, np.ndarray] | None property

Get the range of the parameter.

distribution: str property

Get the distribution type.

__init__(value: list | np.ndarray, is_sampled: bool = False, range: tuple[list | np.ndarray, list | np.ndarray] | tuple[float, float] | None = None, distribution: Literal['uniform', 'mvnormal'] = 'uniform', cov: np.ndarray | None = None)

Initialize a IndependentVectorParameter.

Parameters:

Name Type Description Default
value list | ndarray

Initial vector value as list or 1D array of length N.

required
is_sampled bool

Whether to sample this parameter.

False
range tuple[list | ndarray, list | ndarray] | tuple[float, float] | None

Either: - tuple of (low, high) arrays/lists of length N for element-wise bounds - tuple of (low, high) floats to apply same range to all elements - None for no range constraints

None
distribution Literal['uniform', 'mvnormal']

Distribution type - 'uniform' or 'mvnormal'.

'uniform'
cov ndarray | None

Covariance matrix for 'mvnormal' distribution (NxN array). If None and distribution='mvnormal', uses identity matrix.

None

Raises:

Type Description
ValueError

If value is not 1D, range shapes don't match, or distribution parameters are invalid.

Source code in jscip/parameters.py
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
def __init__(
    self,
    value: list | np.ndarray,
    is_sampled: bool = False,
    range: (tuple[list | np.ndarray, list | np.ndarray] | tuple[float, float] | None) = None,
    distribution: Literal["uniform", "mvnormal"] = "uniform",
    cov: np.ndarray | None = None,
):
    """Initialize a IndependentVectorParameter.

    Args:
        value: Initial vector value as list or 1D array of length N.
        is_sampled: Whether to sample this parameter.
        range: Either:
            - tuple of (low, high) arrays/lists of length N for element-wise bounds
            - tuple of (low, high) floats to apply same range to all elements
            - None for no range constraints
        distribution: Distribution type - 'uniform' or 'mvnormal'.
        cov: Covariance matrix for 'mvnormal' distribution (NxN array).
            If None and distribution='mvnormal', uses identity matrix.

    Raises:
        ValueError: If value is not 1D, range shapes don't match, or
            distribution parameters are invalid.
    """
    # Call base class constructor
    super().__init__(is_sampled=is_sampled)

    # Convert value to numpy array and validate
    self._value = self._validate_and_convert_value(value)
    self._shape = self._value.shape

    # Validate and store range
    self._range = self._validate_and_convert_range(range, self._shape[0])

    # Validate distribution
    if distribution not in ("uniform", "mvnormal"):
        raise ValueError("distribution must be 'uniform' or 'mvnormal'")
    self._distribution = distribution

    # Validate is_sampled requirements
    if is_sampled and range is None:
        raise ValueError("If is_sampled is True, range must be provided.")

    # Set up distribution for sampling
    self._dist = None
    if is_sampled:
        if distribution == "uniform":
            # For uniform, we'll sample each element independently
            self._dist = None  # Will use element-wise uniform sampling
        elif distribution == "mvnormal":
            # Set up multivariate normal distribution
            if self._range is None:
                raise ValueError("range must be provided for mvnormal distribution")
            # Use mean as midpoint of range
            mean = (self._range[0] + self._range[1]) / 2.0

            # Use provided covariance or identity
            if cov is None:
                # Default: identity covariance (independent components)
                cov_matrix = np.eye(self._shape[0])
            else:
                cov_matrix = np.asarray(cov)
                if cov_matrix.shape != (self._shape[0], self._shape[0]):
                    raise ValueError(
                        f"Covariance matrix must be {self._shape[0]}x{self._shape[0]}, "
                        f"got {cov_matrix.shape}"
                    )

            self._dist = stats.multivariate_normal(mean=mean, cov=cov_matrix)
            self._cov = cov_matrix
        else:
            raise ValueError(f"Unknown distribution: {distribution}")

    logger.debug(
        "Initialized IndependentVectorParameter with shape %s, is_sampled %s, distribution %s",
        self._shape,
        is_sampled,
        distribution,
    )

sample(size: int | None = None) -> np.ndarray

Sample from the parameter's distribution.

If is_sampled is True, draws from the configured distribution. Otherwise, returns the fixed value.

Parameters:

Name Type Description Default
size int | None

Optional number of samples. If omitted, returns a single sample with shape matching the parameter shape.

None

Returns:

Type Description
ndarray

numpy.ndarray: If size is None, returns array of shape (N,). If size is provided, returns array of shape (size, N).

Source code in jscip/parameters.py
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
def sample(self, size: int | None = None) -> np.ndarray:
    """Sample from the parameter's distribution.

    If `is_sampled` is True, draws from the configured distribution.
    Otherwise, returns the fixed `value`.

    Args:
        size: Optional number of samples. If omitted, returns a single
            sample with shape matching the parameter shape.

    Returns:
        numpy.ndarray: If size is None, returns array of shape (N,).
            If size is provided, returns array of shape (size, N).
    """
    if not self.is_sampled:
        if size is None:
            result = self.value.copy()
        else:
            result = np.tile(self.value, (size, 1))
        logger.debug("Returned fixed value from IndependentVectorParameter: %s", result)
        return result

    if self._distribution == "uniform":
        # Sample each element independently from uniform distribution
        assert self._range is not None  # Type guard for mypy
        low, high = self._range
        if size is None:
            result = np.random.uniform(low, high)
        else:
            result = np.random.uniform(low, high, size=(size, len(low)))
    elif self._distribution == "mvnormal":
        # Sample from multivariate normal
        assert self._dist is not None  # Type guard for mypy
        assert self._range is not None  # Type guard for mypy
        if size is None:
            sample = self._dist.rvs()
            # Clip to range
            low, high = self._range
            result = np.clip(sample, low, high)
        else:
            samples = self._dist.rvs(size=size)
            # Clip to range
            low, high = self._range
            result = np.clip(samples, low, high)
    else:
        raise ValueError(f"Unknown distribution: {self._distribution}")

    logger.debug("Sampled value from IndependentVectorParameter: %s", result)
    return result

copy() -> IndependentVectorParameter

Return a copy preserving configuration.

Returns:

Name Type Description
IndependentVectorParameter IndependentVectorParameter

A new parameter with the same configuration.

Source code in jscip/parameters.py
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
def copy(self) -> IndependentVectorParameter:
    """Return a copy preserving configuration.

    Returns:
        IndependentVectorParameter: A new parameter with the same configuration.
    """
    # Reconstruct range in original format
    if self._range is not None:
        range_copy = (self._range[0].copy(), self._range[1].copy())
    else:
        range_copy = None

    # Get covariance if mvnormal
    cov_copy = None
    if self._distribution == "mvnormal" and hasattr(self, "_cov"):
        cov_copy = self._cov.copy()

    result = IndependentVectorParameter(
        value=self.value.copy(),
        is_sampled=self.is_sampled,
        range=range_copy,
        distribution=self._distribution,  # Use _distribution to get Literal type
        cov=cov_copy,
    )
    logger.debug("Copied IndependentVectorParameter: %s", result)
    return result