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
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
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 "
                    f"{expected_len}."
                )
        else:
            if len(theta) != len(self.parameters):
                raise ValueError(
                    f"Array length {len(theta)} does not match number of parameters "
                    f"{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 "
                    f"{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 "
            f"{self._max_attempts} attempts."
        )

    def sample(
        self, size: int | tuple[int, ...] | 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
        samples = None
        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 "
                        f"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 "
                        f"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 "
                        f"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 "
                        f"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 isinstance(input, list):
            samples = input
        else:
            raise ValueError(
                "Samples must be a list of ParameterSet instances, "
                "a numpy array, or a ParameterSet."
            )

        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 compute_hypergrid(self) -> list[ParameterSet]:
        """Generate a hypergrid over sampled scalar parameters.

        Creates a Cartesian product of grid points for all sampled scalar
        parameters with grid_points specified. Parameters without grid_points
        are excluded from the hypergrid generation.

        Returns:
            List of ParameterSet instances covering the Cartesian product
            of all active parameter grid points.

        Raises:
            ValueError: If no sampled parameters have grid_points specified,
                if grid configuration is invalid, or if vector parameters
                are marked for sampling.
        """
        from itertools import product

        # Validate and extract active grid parameters
        active_params = self._extract_active_grid_parameters()

        if not active_params:
            raise ValueError(
                "No sampled parameters have grid_points specified. "
                "At least one sampled parameter must have grid_points set."
            )

        # Generate grid points for each active parameter
        grid_values = {}
        for name, param in active_params.items():
            grid_values[name] = self._generate_grid_points(param)

        # Create Cartesian product
        param_names = list(grid_values.keys())
        param_value_lists = [grid_values[name] for name in param_names]
        grid_points = product(*param_value_lists)

        n_total_points = 1
        for values in param_value_lists:
            n_total_points *= len(values)

        logger.info(
            "Generating hypergrid with %d points across %d parameters",
            n_total_points,
            len(active_params),
        )

        # Generate ParameterSet instances
        results = []
        for point in grid_points:
            try:
                # Create parameter values for this grid point
                param_dict = {}

                # Set grid parameters
                for i, name in enumerate(param_names):
                    param_dict[name] = point[i]

                # Set non-sampled independent parameters to their current values
                for name, param in self.parameters.items():
                    if not param.is_sampled and hasattr(param, "value"):
                        param_dict[name] = param.value

                # Create ParameterSet and compute derived parameters
                base_set = ParameterSet(param_dict)

                # Compute derived parameters iteratively to handle dependencies
                current_dict = dict(base_set)
                max_iterations = len(self.parameters)

                for iteration in range(max_iterations):
                    new_derived = {}
                    for name, param in self.parameters.items():
                        if hasattr(param, "compute") and name not in current_dict:
                            try:
                                temp_set = ParameterSet(current_dict)
                                new_derived[name] = param.compute(temp_set)
                            except KeyError:
                                continue

                    if not new_derived:
                        break

                    current_dict.update(new_derived)

                # Create final ParameterSet
                final_set = ParameterSet(current_dict)
                ordered_set = self.order(final_set)
                results.append(ordered_set)

            except Exception as e:
                raise RuntimeError(f"Failed to generate grid point {point}: {e}") from e

        logger.info("Successfully generated %d hypergrid points", len(results))
        return results

    def _extract_active_grid_parameters(
        self,
    ) -> dict[str, IndependentScalarParameter]:
        """Extract sampled scalar parameters with grid_points specified.

        Returns:
            Dictionary mapping parameter names to IndependentScalarParameter
            instances that are active in hypergrid generation.

        Raises:
            ValueError: If vector parameters are marked for sampling.
        """
        # Check for vector parameters marked for sampling
        vector_sampled = [
            name
            for name, param in self.parameters.items()
            if param.is_sampled and hasattr(param, "shape")
        ]

        if vector_sampled:
            raise ValueError(
                f"Vector-valued parameters cannot be used in hypergrid: "
                f"{vector_sampled}. Only scalar parameters are supported."
            )

        # Extract sampled scalar parameters with grid_points
        active_params = {}
        for name, param in self.parameters.items():
            if (
                param.is_sampled
                and isinstance(param, IndependentScalarParameter)
                and param.grid_points is not None
            ):
                active_params[name] = param

        return active_params

    def _generate_grid_points(self, param: IndependentScalarParameter) -> list[float]:
        """Generate grid points for a parameter based on its configuration.

        Args:
            param: IndependentScalarParameter with grid configuration.

        Returns:
            List of grid point values.

        Raises:
            ValueError: If grid configuration is invalid.
        """
        if param.grid_points is None:
            raise ValueError(f"Parameter {param} has no grid_points specified.")

        if isinstance(param.grid_points, int):
            # Generate points automatically
            if param.range is None:
                raise ValueError(
                    f"Parameter {param} must have range specified when grid_points is int."
                )

            low, high = param.range
            if param.grid_scale == "linear":
                points = np.linspace(low, high, param.grid_points).tolist()
            elif param.grid_scale == "log":
                if low <= 0 or high <= 0:
                    raise ValueError(
                        f"Parameter {param} must have positive range for logarithmic scaling."
                    )
                points = np.logspace(
                    np.log10(low), np.log10(high), param.grid_points
                ).tolist()
            else:
                raise ValueError(f"Invalid grid_scale: {param.grid_scale}")

        elif isinstance(param.grid_points, Sequence):
            # Use explicit point list
            points = list(param.grid_points)

            # Validate logarithmic scaling requirements
            if param.grid_scale == "log":
                if any(x <= 0 for x in points):
                    raise ValueError(
                        f"Parameter {param} grid points must be positive for logarithmic scaling."
                    )

        else:
            raise ValueError(f"Invalid grid_points type: {type(param.grid_points)}")

        # Apply type constraints to generated grid points
        if param.param_type is not None:
            if param.param_type == "int":
                points = [int(x) for x in points]
            elif param.param_type == "float":
                points = [float(x) for x in points]
            elif param.param_type == "bool":
                points = [bool(x) for x in points]

        return points

    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
186
187
188
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
190
191
192
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
194
195
196
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
198
199
200
201
202
203
204
205
206
207
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
209
210
211
212
213
214
215
216
217
218
219
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
235
236
237
238
239
240
241
242
243
244
245
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
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
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
276
277
278
279
280
281
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
283
284
285
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
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
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
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
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
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
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
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
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 "
                f"{expected_len}."
            )
    else:
        if len(theta) != len(self.parameters):
            raise ValueError(
                f"Array length {len(theta)} does not match number of parameters "
                f"{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 "
                f"{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[int, ...] | None = None) -> ParameterSet | pd.DataFrame | np.ndarray

Sample parameter sets or theta arrays.

Parameters:

Name Type Description Default
size int | tuple[int, ...] | 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
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
def sample(
    self, size: int | tuple[int, ...] | 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
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
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
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
727
728
729
730
731
732
733
734
735
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
    samples = None
    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 "
                    f"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 "
                    f"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 "
                    f"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 "
                    f"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 isinstance(input, list):
        samples = input
    else:
        raise ValueError(
            "Samples must be a list of ParameterSet instances, "
            "a numpy array, or a ParameterSet."
        )

    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
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
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

compute_hypergrid() -> list[ParameterSet]

Generate a hypergrid over sampled scalar parameters.

Creates a Cartesian product of grid points for all sampled scalar parameters with grid_points specified. Parameters without grid_points are excluded from the hypergrid generation.

Returns:

Type Description
list[ParameterSet]

List of ParameterSet instances covering the Cartesian product

list[ParameterSet]

of all active parameter grid points.

Raises:

Type Description
ValueError

If no sampled parameters have grid_points specified, if grid configuration is invalid, or if vector parameters are marked for sampling.

Source code in jscip/parameter_bank.py
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
def compute_hypergrid(self) -> list[ParameterSet]:
    """Generate a hypergrid over sampled scalar parameters.

    Creates a Cartesian product of grid points for all sampled scalar
    parameters with grid_points specified. Parameters without grid_points
    are excluded from the hypergrid generation.

    Returns:
        List of ParameterSet instances covering the Cartesian product
        of all active parameter grid points.

    Raises:
        ValueError: If no sampled parameters have grid_points specified,
            if grid configuration is invalid, or if vector parameters
            are marked for sampling.
    """
    from itertools import product

    # Validate and extract active grid parameters
    active_params = self._extract_active_grid_parameters()

    if not active_params:
        raise ValueError(
            "No sampled parameters have grid_points specified. "
            "At least one sampled parameter must have grid_points set."
        )

    # Generate grid points for each active parameter
    grid_values = {}
    for name, param in active_params.items():
        grid_values[name] = self._generate_grid_points(param)

    # Create Cartesian product
    param_names = list(grid_values.keys())
    param_value_lists = [grid_values[name] for name in param_names]
    grid_points = product(*param_value_lists)

    n_total_points = 1
    for values in param_value_lists:
        n_total_points *= len(values)

    logger.info(
        "Generating hypergrid with %d points across %d parameters",
        n_total_points,
        len(active_params),
    )

    # Generate ParameterSet instances
    results = []
    for point in grid_points:
        try:
            # Create parameter values for this grid point
            param_dict = {}

            # Set grid parameters
            for i, name in enumerate(param_names):
                param_dict[name] = point[i]

            # Set non-sampled independent parameters to their current values
            for name, param in self.parameters.items():
                if not param.is_sampled and hasattr(param, "value"):
                    param_dict[name] = param.value

            # Create ParameterSet and compute derived parameters
            base_set = ParameterSet(param_dict)

            # Compute derived parameters iteratively to handle dependencies
            current_dict = dict(base_set)
            max_iterations = len(self.parameters)

            for iteration in range(max_iterations):
                new_derived = {}
                for name, param in self.parameters.items():
                    if hasattr(param, "compute") and name not in current_dict:
                        try:
                            temp_set = ParameterSet(current_dict)
                            new_derived[name] = param.compute(temp_set)
                        except KeyError:
                            continue

                if not new_derived:
                    break

                current_dict.update(new_derived)

            # Create final ParameterSet
            final_set = ParameterSet(current_dict)
            ordered_set = self.order(final_set)
            results.append(ordered_set)

        except Exception as e:
            raise RuntimeError(f"Failed to generate grid point {point}: {e}") from e

    logger.info("Successfully generated %d hypergrid points", len(results))
    return results

pretty_print() -> None

Print a human-readable summary of the bank configuration.

Source code in jscip/parameter_bank.py
969
970
971
972
973
974
975
976
977
978
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
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
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: Any, **kwargs: Any) -> 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, deep: bool = True) -> ParameterSet:
        """Return a copy of this parameter set.

        Args:
            deep: Whether to perform a deep copy (default: True).

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

        Note:
            Numpy arrays are always deep copied to prevent unintended mutations,
            regardless of the deep parameter.
        """
        # 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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(deep: bool = True) -> ParameterSet

Return a copy of this parameter set.

Parameters:

Name Type Description Default
deep bool

Whether to perform a deep copy (default: True).

True

Returns:

Name Type Description
ParameterSet ParameterSet

A new instance with the same values.

Note

Numpy arrays are always deep copied to prevent unintended mutations, regardless of the deep parameter.

Source code in jscip/parameter_set.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def copy(self, deep: bool = True) -> ParameterSet:
    """Return a copy of this parameter set.

    Args:
        deep: Whether to perform a deep copy (default: True).

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

    Note:
        Numpy arrays are always deep copied to prevent unintended mutations,
        regardless of the deep parameter.
    """
    # 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
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
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
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
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: Callable[[ParameterSet], Any]) -> None:
        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: ParameterSet) -> Any:
        """Compute the derived value for a given parameter set.

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

    def copy(self) -> DerivedParameter:
        """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: ParameterSet) -> Any

Compute the derived value for a given parameter set.

Must be implemented by subclasses.

Source code in jscip/parameters.py
157
158
159
160
161
162
def compute(self, parameters: ParameterSet) -> Any:
    """Compute the derived value for a given parameter set.

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

copy() -> DerivedParameter

Return a copy of this parameter.

Must be implemented by subclasses.

Source code in jscip/parameters.py
164
165
166
167
168
169
def copy(self) -> DerivedParameter:
    """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
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
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: Callable[[ParameterSet], Any],
        param_type: Literal["int", "float", "bool"] | None = None,
    ) -> None:
        super().__init__(function)
        self._param_type = param_type
        logger.debug(
            "Initialized DerivedScalarParameter with function %s, " "param_type %s",
            self.function,
            param_type,
        )

    @property
    def param_type(self) -> Literal["int", "float", "bool"] | None:
        """Get the explicit type override for this derived parameter."""
        return self._param_type

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

    def compute(self, parameters: ParameterSet) -> Any:
        """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)

        # Apply type constraint if param_type is specified
        if self._param_type is not None:
            if self._param_type == "int":
                result = int(result)
            elif self._param_type == "float":
                result = float(result)
            elif self._param_type == "bool":
                result = bool(result)

        logger.debug("Computed value of DerivedScalarParameter: %s", result)
        return 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, param_type=self.param_type
        )
        logger.debug("Copied DerivedScalarParameter: %s", result)
        return result

param_type: Literal['int', 'float', 'bool'] | None property

Get the explicit type override for this derived parameter.

compute(parameters: ParameterSet) -> Any

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 Any

The computed scalar value.

Source code in jscip/parameters.py
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
def compute(self, parameters: ParameterSet) -> Any:
    """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)

    # Apply type constraint if param_type is specified
    if self._param_type is not None:
        if self._param_type == "int":
            result = int(result)
        elif self._param_type == "float":
            result = float(result)
        elif self._param_type == "bool":
            result = bool(result)

    logger.debug("Computed value of DerivedScalarParameter: %s", result)
    return 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
472
473
474
475
476
477
478
479
480
481
482
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, param_type=self.param_type
    )
    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
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
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
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
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
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
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
579
580
581
582
583
584
585
586
587
588
589
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.

param_type Literal['int', 'float', 'bool'] | None

Optional explicit type override ('int', 'float', 'bool').

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
 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
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.
        param_type: Optional explicit type override ('int', 'float', 'bool').
    """

    def __init__(
        self,
        is_sampled: bool = False,
        param_type: Literal["int", "float", "bool"] | None = None,
    ):
        self._is_sampled = is_sampled
        if param_type is not None and param_type not in ["int", "float", "bool"]:
            raise ValueError("param_type must be 'int', 'float', 'bool', or None")
        self._param_type = param_type

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

    @property
    def param_type(self) -> Literal["int", "float", "bool"] | None:
        """Get the explicit type override for this parameter."""
        return self._param_type

    @param_type.setter
    def param_type(self, param_type: Literal["int", "float", "bool"] | None) -> None:
        """Set the explicit type override for this parameter."""
        if param_type is not None and param_type not in ["int", "float", "bool"]:
            raise ValueError("param_type must be 'int', 'float', 'bool', or None")
        self._param_type = param_type

    def _detect_type_from_value(self, value: Any) -> Literal["int", "float", "bool"]:
        """Detect type from a value.

        Args:
            value: The value to detect type from.

        Returns:
            Detected type as 'int', 'float', or 'bool'.
        """
        if isinstance(value, bool):
            return "bool"
        elif isinstance(value, int) and not isinstance(value, bool):
            return "int"
        elif isinstance(value, float):
            return "float"
        elif isinstance(value, np.ndarray):
            # For numpy arrays, detect from the first element
            if value.size == 0:
                raise ValueError("Cannot detect type from empty array")
            first_element = value.flat[0]
            return self._detect_type_from_value(first_element)
        elif isinstance(value, (list, tuple)):
            # For sequences, detect from the first element
            if len(value) == 0:
                raise ValueError("Cannot detect type from empty sequence")
            return self._detect_type_from_value(value[0])
        else:
            # Try to convert to int first, then float
            try:
                int(value)
                return "int"
            except (ValueError, TypeError):
                try:
                    float(value)
                    return "float"
                except (ValueError, TypeError):
                    raise ValueError(f"Cannot detect type from value: {value}")

    def _apply_type_constraint(self, value: Any) -> Any:
        """Apply type constraint to a value.

        Args:
            value: The value to apply type constraint to.

        Returns:
            Value converted to the appropriate type.
        """
        target_type = self._param_type or self._detect_type_from_value(value)

        if target_type == "int":
            return int(value)
        elif target_type == "float":
            return float(value)
        elif target_type == "bool":
            return bool(value)
        else:
            raise ValueError(f"Unknown target type: {target_type}")

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

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

    def copy(self) -> IndependentParameter:
        """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.

param_type: Literal['int', 'float', 'bool'] | None property writable

Get the explicit type override for this parameter.

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

Sample from the parameter's distribution.

Must be implemented by subclasses.

Source code in jscip/parameters.py
120
121
122
123
124
125
def sample(self, size: int | None = None) -> float | np.ndarray:
    """Sample from the parameter's distribution.

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

copy() -> IndependentParameter

Return a copy of this parameter.

Must be implemented by subclasses.

Source code in jscip/parameters.py
127
128
129
130
131
132
def copy(self) -> IndependentParameter:
    """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 Any

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.

grid_points int | Sequence[float] | None

Number of grid points, explicit point list, or None for hypergrid generation.

grid_scale Literal['linear', 'log']

Spacing scale for hypergrid generation ('linear' or 'log').

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
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
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.
        grid_points: Number of grid points, explicit point list, or None for
            hypergrid generation.
        grid_scale: Spacing scale for hypergrid generation ('linear' or 'log').

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

    def __init__(
        self,
        value: Any,
        is_sampled: bool = False,
        range: tuple[float, float] | None = None,
        grid_points: int | Sequence[float] | None = None,
        grid_scale: Literal["linear", "log"] = "linear",
        param_type: Literal["int", "float", "bool"] | None = None,
    ):
        super().__init__(is_sampled=is_sampled, param_type=param_type)

        # Apply type constraint to the initial value
        if param_type is not None:
            value = self._apply_type_constraint(value)
        else:
            # Auto-detect and set type from value
            detected_type = self._detect_type_from_value(value)
            self._param_type = detected_type
            value = self._apply_type_constraint(value)

        self._validate_value_and_range(value, range)
        self._value = value
        self._range = range

        # Validate and store grid configuration
        self._validate_grid_config(grid_points, grid_scale)
        self._grid_points = grid_points
        self._grid_scale = grid_scale

        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) -> Any:
        """Get the value of the parameter."""
        return self._value

    @value.setter
    def value(self, value: Any) -> None:
        # Apply type constraint
        value = self._apply_type_constraint(value)
        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)

    @property
    def grid_points(self) -> int | Sequence[float] | None:
        """Get the grid points configuration."""
        return self._grid_points

    @grid_points.setter
    def grid_points(self, grid_points: int | Sequence[float] | None) -> None:
        self._validate_grid_config(grid_points, self._grid_scale)
        self._grid_points = grid_points
        logger.debug("Set grid_points of IndependentScalarParameter to %s", grid_points)

    @property
    def grid_scale(self) -> Literal["linear", "log"]:
        """Get the grid scale configuration."""
        return self._grid_scale

    @grid_scale.setter
    def grid_scale(self, grid_scale: Literal["linear", "log"]) -> None:
        self._validate_grid_config(self._grid_points, grid_scale)
        self._grid_scale = grid_scale
        logger.debug("Set grid_scale of IndependentScalarParameter to %s", grid_scale)

    def __repr__(self) -> str:
        return (
            f"IndependentScalarParameter(value={self.value}, range={self.range}, "
            f"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 _validate_grid_config(
        self,
        grid_points: int | Sequence[float] | None,
        grid_scale: Literal["linear", "log"],
    ) -> None:
        """Validate grid configuration parameters.

        Args:
            grid_points: Number of points, explicit point list, or None.
            grid_scale: Spacing scale ('linear' or 'log').

        Raises:
            ValueError: If grid configuration is invalid.
        """
        if grid_points is not None:
            if isinstance(grid_points, int):
                if grid_points <= 0:
                    raise ValueError(
                        "grid_points must be positive when specified as int."
                    )
            elif isinstance(grid_points, Sequence):
                if len(grid_points) == 0:
                    raise ValueError("grid_points sequence cannot be empty.")
                if not all(isinstance(x, (int, float)) for x in grid_points):
                    raise ValueError(
                        "grid_points sequence must contain only numeric values."
                    )
            else:
                raise ValueError("grid_points must be int, Sequence[float], or None.")

        if grid_scale not in ["linear", "log"]:
            raise ValueError("grid_scale must be 'linear' or 'log'.")

        # Validate logarithmic scale requirements
        if grid_scale == "log" and grid_points is not None:
            if isinstance(grid_points, int) and self.range is not None:
                if self.range[0] <= 0 or self.range[1] <= 0:
                    raise ValueError(
                        "Range must be positive for logarithmic grid scaling."
                    )
            elif isinstance(grid_points, Sequence):
                if any(x <= 0 for x in grid_points):
                    raise ValueError(
                        "Grid points must be positive for logarithmic scaling."
                    )

    def sample(self, size: int | None = None) -> float | np.ndarray:
        """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 value if `size is None`,
                otherwise a NumPy array of samples.
        """
        if self.is_sampled:
            result = self._distribution.rvs(size=size)
        else:
            result = self.value

        # Apply type constraint to sampled values
        if size is None:
            result = self._apply_type_constraint(result)
        else:
            # For arrays, apply type constraint element-wise
            result = np.array([self._apply_type_constraint(x) for x in result])

        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,
            sampling flag, and grid configuration.
        """
        result = IndependentScalarParameter(
            value=self.value,
            is_sampled=self.is_sampled,
            range=self.range,
            grid_points=self.grid_points,
            grid_scale=self.grid_scale,
            param_type=self.param_type,
        )
        logger.debug("Copied IndependentScalarParameter: %s", result)
        return result

value: Any property writable

Get the value of the parameter.

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

Get the range of the parameter.

grid_points: int | Sequence[float] | None property writable

Get the grid points configuration.

grid_scale: Literal['linear', 'log'] property writable

Get the grid scale configuration.

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

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 | ndarray

float | numpy.ndarray: A single value if size is None, otherwise a NumPy array of samples.

Source code in jscip/parameters.py
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
def sample(self, size: int | None = None) -> float | np.ndarray:
    """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 value if `size is None`,
            otherwise a NumPy array of samples.
    """
    if self.is_sampled:
        result = self._distribution.rvs(size=size)
    else:
        result = self.value

    # Apply type constraint to sampled values
    if size is None:
        result = self._apply_type_constraint(result)
    else:
        # For arrays, apply type constraint element-wise
        result = np.array([self._apply_type_constraint(x) for x in result])

    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

sampling flag, and grid configuration.

Source code in jscip/parameters.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
def copy(self) -> IndependentScalarParameter:
    """Return a shallow copy preserving configuration.

    Returns:
        IndependentScalarParameter: A new parameter with the same value, range,
        sampling flag, and grid configuration.
    """
    result = IndependentScalarParameter(
        value=self.value,
        is_sampled=self.is_sampled,
        range=self.range,
        grid_points=self.grid_points,
        grid_scale=self.grid_scale,
        param_type=self.param_type,
    )
    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
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
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
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[Any] | np.ndarray,
        is_sampled: bool = False,
        range: (
            tuple[list[float] | np.ndarray, list[float] | np.ndarray]
            | tuple[float, float]
            | None
        ) = None,
        distribution: Literal["uniform", "mvnormal"] = "uniform",
        cov: np.ndarray | None = None,
        param_type: Literal["int", "float", "bool"] | 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, param_type=param_type)

        # First validate and convert to numpy array to check shape
        temp_value = self._validate_and_convert_value(value)
        self._shape = temp_value.shape

        # Apply type constraints to vector elements
        if param_type is not None:
            # Apply type constraint to all elements
            value = np.array([self._apply_type_constraint(x) for x in temp_value])
        else:
            # Auto-detect type from first element and apply to all
            if temp_value.size > 0:
                # For auto-detection, check the original input type
                # before numpy conversion
                if isinstance(value, (list, tuple)) and len(value) > 0:
                    detected_type = self._detect_type_from_value(value[0])
                else:
                    detected_type = self._detect_type_from_value(temp_value.flat[0])
                self._param_type = detected_type
                value = np.array([self._apply_type_constraint(x) for x in temp_value])
            else:
                value = temp_value

        # Convert value to numpy array with appropriate dtype
        self._value = self._validate_and_convert_value(value)

        # 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 self._shape[0] == 0:
            raise ValueError(
                "Cannot sample from a zero-length IndependentVectorParameter."
            )
        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[Any] | 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.
        """
        # Convert to numpy array with appropriate dtype based on param_type
        if self._param_type == "int":
            arr = np.asarray(value, dtype=int)
        elif self._param_type == "float":
            arr = np.asarray(value, dtype=float)
        elif self._param_type == "bool":
            arr = np.asarray(value, dtype=bool)
        else:
            # Don't force float when auto-detecting - let numpy infer
            arr = np.asarray(value)

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

        return arr

    def _validate_and_convert_range(
        self,
        range: (
            tuple[list[float] | np.ndarray, list[float] | 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[Any] | 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.
        """
        # Apply type constraints to vector elements
        if self._param_type is not None:
            # Apply type constraint to all elements
            if isinstance(value, list):
                value = [self._apply_type_constraint(x) for x in value]
            else:
                value = np.array([self._apply_type_constraint(x) for x in value])

        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}")

        # Apply type constraints to sampled values
        if self._param_type is not None:
            if size is None:
                result = np.array([self._apply_type_constraint(x) for x in result])
            else:
                # Apply type constraints element-wise to each sample
                result = np.array(
                    [
                        [self._apply_type_constraint(x) for x in sample]
                        for sample in result
                    ]
                )

        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,
            param_type=self.param_type,
        )
        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[Any] | np.ndarray, is_sampled: bool = False, range: tuple[list[float] | np.ndarray, list[float] | np.ndarray] | tuple[float, float] | None = None, distribution: Literal['uniform', 'mvnormal'] = 'uniform', cov: np.ndarray | None = None, param_type: Literal['int', 'float', 'bool'] | None = None)

Initialize a IndependentVectorParameter.

Parameters:

Name Type Description Default
value list[Any] | 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[float] | ndarray, list[float] | 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
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
def __init__(
    self,
    value: list[Any] | np.ndarray,
    is_sampled: bool = False,
    range: (
        tuple[list[float] | np.ndarray, list[float] | np.ndarray]
        | tuple[float, float]
        | None
    ) = None,
    distribution: Literal["uniform", "mvnormal"] = "uniform",
    cov: np.ndarray | None = None,
    param_type: Literal["int", "float", "bool"] | 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, param_type=param_type)

    # First validate and convert to numpy array to check shape
    temp_value = self._validate_and_convert_value(value)
    self._shape = temp_value.shape

    # Apply type constraints to vector elements
    if param_type is not None:
        # Apply type constraint to all elements
        value = np.array([self._apply_type_constraint(x) for x in temp_value])
    else:
        # Auto-detect type from first element and apply to all
        if temp_value.size > 0:
            # For auto-detection, check the original input type
            # before numpy conversion
            if isinstance(value, (list, tuple)) and len(value) > 0:
                detected_type = self._detect_type_from_value(value[0])
            else:
                detected_type = self._detect_type_from_value(temp_value.flat[0])
            self._param_type = detected_type
            value = np.array([self._apply_type_constraint(x) for x in temp_value])
        else:
            value = temp_value

    # Convert value to numpy array with appropriate dtype
    self._value = self._validate_and_convert_value(value)

    # 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 self._shape[0] == 0:
        raise ValueError(
            "Cannot sample from a zero-length IndependentVectorParameter."
        )
    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
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
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}")

    # Apply type constraints to sampled values
    if self._param_type is not None:
        if size is None:
            result = np.array([self._apply_type_constraint(x) for x in result])
        else:
            # Apply type constraints element-wise to each sample
            result = np.array(
                [
                    [self._apply_type_constraint(x) for x in sample]
                    for sample in result
                ]
            )

    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
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
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,
        param_type=self.param_type,
    )
    logger.debug("Copied IndependentVectorParameter: %s", result)
    return result