Skip to content

API Reference

DerivedParameter

A read-only parameter computed from other parameters.

A DerivedParameter 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/main.py
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
class DerivedParameter:
    """A read-only parameter computed from other parameters.

    A ``DerivedParameter`` 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], float], unit: object | None = None) -> None:
        self.function = function
        self._is_sampled = False  # Derived parameters are never considered sampled.
        # Optional unit associated with this derived parameter. Units are not
        # used in internal calculations but can be applied on readout if
        # requested at the bank level.
        self._unit = unit
        if not callable(self.function):
            raise ValueError("Function must be callable.")
        logger.debug("Initialized DerivedParameter with function %s", self.function)

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

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

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

        Returns:
            float: The computed scalar value.

        Raises:
            ValueError: If ``parameters`` is not a ``ParameterSet`` or the
                stored function is not callable.
        """
        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)
        logger.debug("Computed value of DerivedParameter: %s", result)
        return result

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

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

compute(parameters: ParameterSet) -> float

Compute the derived value for a given parameter set.

Parameters:

Name Type Description Default
parameters ParameterSet

The ParameterSet providing inputs to the function.

required

Returns:

Name Type Description
float float

The computed scalar value.

Raises:

Type Description
ValueError

If parameters is not a ParameterSet or the stored function is not callable.

Source code in jscip/main.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
def compute(self, parameters: ParameterSet) -> float:
    """Compute the derived value for a given parameter set.

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

    Returns:
        float: The computed scalar value.

    Raises:
        ValueError: If ``parameters`` is not a ``ParameterSet`` or the
            stored function is not callable.
    """
    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)
    logger.debug("Computed value of DerivedParameter: %s", result)
    return result

copy() -> DerivedParameter

Return a shallow copy preserving the underlying function.

Returns:

Name Type Description
DerivedParameter DerivedParameter

A new wrapper around the same function.

Source code in jscip/main.py
454
455
456
457
458
459
460
461
462
def copy(self) -> DerivedParameter:
    """Return a shallow copy preserving the underlying function.

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

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:

Name Type Description
value float

Current scalar value of the parameter.

is_sampled float

Whether this parameter will be sampled from a distribution.

range tuple[float, float] | None

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

Raises:

Type Description
ValueError

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

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

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

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

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

    def __init__(
        self,
        value: float,
        is_sampled: bool = False,
        range: tuple[float, float] | None = None,
        distribution: object | None = None,
        unit: object | None = None,
        param_type: type | None = None,
    ):
        self._validate_value_and_range(value, range)
        self._value = value
        self._range = range
        self._is_sampled = is_sampled

        # Infer the parameter type from the provided value when not specified
        # explicitly. Currently only float and int are supported.
        if param_type is None:
            if isinstance(value, int):
                self._type = int
            elif isinstance(value, float):
                self._type = float
            else:
                raise ValueError("param_type must be float or int, or value must be int/float.")
        else:
            if param_type not in (float, int):
                raise ValueError("param_type must be float or int.")
            self._type = param_type

        # Store the raw distribution configuration/object so that copy() can
        # faithfully reproduce the sampling behavior.
        self._distribution_config = distribution

        # Optional unit associated with this parameter. Units are not used in
        # internal calculations but can be applied on readout if requested.
        self._unit = unit

        # Configure the underlying scipy.stats distribution. By default, when
        # no explicit distribution is provided, a uniform distribution over
        # ``range`` is used (for backwards compatibility). When
        # ``distribution`` is provided, it may be either (a) a configuration
        # dict describing a common distribution or (b) a user-supplied
        # scipy.stats frozen distribution object.
        self._distribution = None
        if is_sampled:
            if distribution is None:
                if range is None or len(range) != 2:
                    raise ValueError(
                        "If is_sampled is True, range must be a tuple of two numeric values."
                    )
                # Default behavior: uniform over the given range.
                self._distribution = stats.uniform(
                    loc=self.range[0], scale=self.range[1] - self.range[0]
                )
            else:
                # Accept either a user-supplied scipy.stats frozen
                # distribution (duck-typed via rvs attribute) or a small
                # configuration dict specifying a common distribution.
                if hasattr(distribution, "rvs"):
                    self._distribution = distribution
                elif isinstance(distribution, dict):
                    kind = distribution.get("kind")
                    if kind == "uniform":
                        # Allow overriding uniform via config; fall back to
                        # the explicit range bounds when present.
                        if range is not None and len(range) == 2:
                            low, high = range
                        else:
                            low = distribution.get("low")
                            high = distribution.get("high")
                        if low is None or high is None:
                            raise ValueError(
                                "Uniform distribution requires 'low' and 'high' when range is not provided."
                            )
                        self._distribution = stats.uniform(
                            loc=low, scale=high - low
                        )
                    elif kind == "normal":
                        if self._type is int:
                            raise ValueError(
                                "Integer parameters cannot use a normal distribution configuration."
                            )
                        loc = distribution.get("loc", value)
                        scale = distribution.get("scale", 1.0)
                        self._distribution = stats.norm(loc=loc, scale=scale)
                    elif kind == "lognormal":
                        if self._type is int:
                            raise ValueError(
                                "Integer parameters cannot use a lognormal distribution configuration."
                            )
                        s = distribution.get("s")
                        if s is None:
                            raise ValueError(
                                "Lognormal distribution configuration must include parameter 's'."
                            )
                        scale = distribution.get("scale", 1.0)
                        self._distribution = stats.lognorm(s=s, scale=scale)
                    elif kind == "exponential":
                        if self._type is int:
                            raise ValueError(
                                "Integer parameters cannot use an exponential distribution configuration."
                            )
                        # Parameterized by scale (1 / rate).
                        scale = distribution.get("scale", 1.0)
                        self._distribution = stats.expon(scale=scale)
                    elif kind == "gamma":
                        if self._type is int:
                            raise ValueError(
                                "Integer parameters cannot use a gamma distribution configuration."
                            )
                        a = distribution.get("a")
                        if a is None:
                            raise ValueError(
                                "Gamma distribution configuration must include shape parameter 'a'."
                            )
                        scale = distribution.get("scale", 1.0)
                        self._distribution = stats.gamma(a=a, scale=scale)
                    elif kind == "bernoulli":
                        p = distribution.get("p")
                        if p is None:
                            raise ValueError(
                                "Bernoulli distribution configuration must include parameter 'p'."
                            )
                        self._distribution = stats.bernoulli(p)
                    elif kind == "poisson":
                        if self._type is not int:
                            raise ValueError(
                                "Poisson distribution is only supported for integer parameters."
                            )
                        mu = distribution.get("mu")
                        if mu is None:
                            raise ValueError(
                                "Poisson distribution configuration must include parameter 'mu'."
                            )
                        self._distribution = stats.poisson(mu=mu)
                    elif kind == "binomial":
                        if self._type is not int:
                            raise ValueError(
                                "Binomial distribution is only supported for integer parameters."
                            )
                        n = distribution.get("n")
                        p = distribution.get("p")
                        if n is None or p is None:
                            raise ValueError(
                                "Binomial distribution configuration must include parameters 'n' and 'p'."
                            )
                        self._distribution = stats.binom(n=n, p=p)
                    elif kind == "discrete_uniform":
                        if self._type is not int:
                            raise ValueError(
                                "Discrete uniform distribution is only supported for integer parameters."
                            )
                        low = distribution.get("low")
                        high = distribution.get("high")
                        if low is None or high is None:
                            raise ValueError(
                                "Discrete uniform distribution requires 'low' and 'high'."
                            )
                        # stats.randint is defined on [low, high), so use high+1 for inclusive bounds.
                        self._distribution = stats.randint(low, high + 1)
                    elif kind == "geometric":
                        if self._type is not int:
                            raise ValueError(
                                "Geometric distribution is only supported for integer parameters."
                            )
                        p = distribution.get("p")
                        if p is None:
                            raise ValueError(
                                "Geometric distribution configuration must include parameter 'p'."
                            )
                        self._distribution = stats.geom(p)
                    else:
                        raise ValueError(
                            f"Unsupported distribution kind: {kind}."
                        )
                else:
                    raise ValueError(
                        "distribution must be either a scipy.stats frozen distribution or a configuration dict."
                    )
        logger.debug(
            "Initialized IndependentParameter with value %s, range %s, is_sampled %s",
            value,
            range,
            is_sampled,
        )

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

    @value.setter
    def value(self, value: float) -> None:
        self._validate_value_and_range(value, self.range)
        self._value = value
        logger.debug("Set value of IndependentParameter 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 IndependentParameter to %s", range)

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

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

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

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

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

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

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

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

        Returns:
            float | numpy.ndarray: A single float if `size is None`, otherwise
            a NumPy array of samples.
        """
        if self._is_sampled:
            result = self._distribution.rvs(size=size)
        else:
            result = self.value

        # Cast to the requested scalar type before applying any units.
        if self._type is int:
            if isinstance(result, np.ndarray):
                result = result.astype(int)
            else:
                result = int(result)
        if return_unit and self._unit is not None:
            # Defer to the unit object's multiplication semantics.
            result = result * self._unit
        logger.debug("Sampled value from IndependentParameter: %s", result)
        return result

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

        Returns:
            IndependentParameter: A new parameter with the same value, range,
            and sampling flag.
        """
        result = IndependentParameter(
            value=self.value,
            is_sampled=self._is_sampled,
            range=self.range,
            distribution=self._distribution_config,
            unit=self._unit,
            param_type=self._type,
        )
        logger.debug("Copied IndependentParameter: %s", result)
        return result

value: float property writable

Get the value of the parameter.

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

Get the range of the parameter.

sample(size: int | None = None, return_unit: bool = False) -> float | np.ndarray | object

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

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

float | ndarray | object

a NumPy array of samples.

Source code in jscip/main.py
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
def sample(self, size: int | None = None, return_unit: bool = False) -> float | np.ndarray | object:
    """Sample from the parameter's distribution.

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

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

    Returns:
        float | numpy.ndarray: A single float if `size is None`, otherwise
        a NumPy array of samples.
    """
    if self._is_sampled:
        result = self._distribution.rvs(size=size)
    else:
        result = self.value

    # Cast to the requested scalar type before applying any units.
    if self._type is int:
        if isinstance(result, np.ndarray):
            result = result.astype(int)
        else:
            result = int(result)
    if return_unit and self._unit is not None:
        # Defer to the unit object's multiplication semantics.
        result = result * self._unit
    logger.debug("Sampled value from IndependentParameter: %s", result)
    return result

copy() -> IndependentParameter

Return a shallow copy preserving configuration.

Returns:

Name Type Description
IndependentParameter IndependentParameter

A new parameter with the same value, range,

IndependentParameter

and sampling flag.

Source code in jscip/main.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def copy(self) -> IndependentParameter:
    """Return a shallow copy preserving configuration.

    Returns:
        IndependentParameter: A new parameter with the same value, range,
        and sampling flag.
    """
    result = IndependentParameter(
        value=self.value,
        is_sampled=self._is_sampled,
        range=self.range,
        distribution=self._distribution_config,
        unit=self._unit,
        param_type=self._type,
    )
    logger.debug("Copied IndependentParameter: %s", result)
    return result

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.

Source code in jscip/main.py
 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
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
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.
    """

    def __init__(
        self,
        parameters: dict[str, IndependentParameter | DerivedParameter] | None = None,
        constraints: list[Callable[[ParameterSet], bool]] | None = None,
        vector_mode: bool = False,
        texnames: dict[str, str] | None = None,
    ) -> None:
        self.parameters = parameters if parameters is not None else {}
        self.constraints = constraints if constraints is not None else []
        # When True, sampling and conversions operate on parameter vectors
        # that contain only sampled independent parameters, in canonical
        # sampled order. When False, conversions expect full configurations.
        self.vector_mode = vector_mode
        self._max_attempts = (
            100  # Default maximum attempts for sampling with constraints
        )
        self.texnames = texnames if texnames is not None else {}

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

        # 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, IndependentParameter) 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 lower_bounds(self) -> np.ndarray:
        """Get the lower bounds of all sampled parameters."""
        return np.array(
            [
                param.range[0]
                for key, param in self.parameters.items()
                if isinstance(param, IndependentParameter) and param._is_sampled
            ]
        )

    @property
    def upper_bounds(self) -> np.ndarray:
        """Get the upper bounds of all sampled parameters."""
        return np.array(
            [
                param.range[1]
                for key, param in self.parameters.items()
                if isinstance(param, IndependentParameter) and param._is_sampled
            ]
        )

    @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) -> IndependentParameter | DerivedParameter:
        """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(),
            vector_mode=self.vector_mode,
            texnames=self.texnames.copy(),
        )
        logger.debug("Copied ParameterBank: %s", result)
        return result

    def get_value(self, key: str) -> float:
        if key in self.parameters:
            return self.parameters[key].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, key: str, parameter: IndependentParameter | DerivedParameter
    ) -> None:
        """Add a new parameter to the bank."""
        if not isinstance(parameter, (IndependentParameter, DerivedParameter)):
            raise ValueError(
                "Parameter must be an instance of IndependentParameter or DerivedParameter."
            )
        if key in self.parameters:
            raise KeyError(f"Parameter '{key}' already exists in the bank.")
        self.parameters[key] = parameter
        logger.debug("Added parameter '%s' to ParameterBank: %s", key, 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_vector: bool | None = None, with_units: bool = False) -> 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 (a
        parameter vector) when ``return_vector=True``.

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

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

        Raises:
            ValueError: If ``return_vector`` is not a boolean.
        """
        if return_vector is None:
            return_vector = (
                self.vector_mode
            )  # default to self.vector_mode if not specified
        if not isinstance(return_vector, bool):
            raise ValueError("return_vector must be a boolean value.")
        p = ParameterSet(
            {
                key: param.value
                for key, param in self.parameters.items()
                if isinstance(param, IndependentParameter)
            }
        )
        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, DerivedParameter)
                },
            }
        )
        p = self.order(p)
        logger.debug(
            "[get_default_values] Default values for all parameters in the bank: %s", p
        )
        if return_vector:
            # Parameter vectors are always returned as plain numeric arrays.
            return self.instance_to_vector(p)
        if with_units:
            # Apply units on readout for independent parameters that have them,
            # constructing a new ParameterSet to avoid dtype mutation warnings.
            data: dict[str, object] = {}
            for key in p.index:
                value = p[key]
                param = self.parameters.get(key)
                if isinstance(param, (IndependentParameter, DerivedParameter)) and getattr(
                    param, "_unit", None
                ) is not None:
                    data[key] = value * param._unit
                else:
                    data[key] = value
            return ParameterSet(data)
        return p

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

        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.

        Raises:
            ValueError: If ``input`` is not a ``ParameterSet`` or list thereof.
        """
        if not isinstance(input, (ParameterSet, list)):
            raise ValueError(
                "Input must be a ParameterSet or a list of ParameterSet objects."
            )
        if isinstance(input, ParameterSet):
            vector = np.array([input[key] for key in self.sampled])
            logger.debug(
                "[instance_to_vector] Converted ParameterSet to numpy array: %s",
                vector,
            )
        else:
            # return a 2D array of shape (n_instances, n_sampled)
            vector = np.vstack(
                [
                    np.array([instance[key] for key in self.sampled])
                    for instance in input
                ]
            )
            logger.debug(
                "[instance_to_vector] Converted list of ParameterSet objects to numpy array: %s",
                vector,
            )
        return vector

    def dataframe_to_vector(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.")
        vector = df[self.sampled].to_numpy()
        return vector

    def vector_to_instance(self, vector: np.ndarray) -> ParameterSet:
        """Convert a parameter vector to a parameter instance.

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

        Args:
            vector: 1D NumPy array.

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

        Raises:
            ValueError: If shapes are inconsistent with ``vector_mode`` or
                if ``vector`` is not a NumPy array.
        """
        if not isinstance(vector, np.ndarray):
            raise ValueError(
                "Input must be a numpy array, instead got: " + str(type(vector))
            )
        # validate length depending on vector_mode
        if self.vector_mode:
            if len(vector) != len(self.sampled):
                raise ValueError(
                    f"Array length {len(vector)} does not match number of sampled parameters {len(self.sampled)}."
                )
        else:
            if len(vector) != len(self.parameters):
                raise ValueError(
                    f"Array length {len(vector)} does not match number of parameters {len(self.parameters)}."
                )
        # vector in this case must be a 1D array
        # Start with defaults
        out = self.get_default_values(return_vector=False)
        if self.vector_mode:
            # vector provides only sampled independent parameters
            for i, key in enumerate(self.sampled):
                out[key] = vector[i]
        else:
            # vector provides values for ALL parameters in canonical order
            if len(vector) != len(self.parameters):
                raise ValueError(
                    f"Array length {len(vector)} does not match number of parameters {len(self.parameters)}."
                )
            for i, key in enumerate(self.names):
                param = self.parameters[key]
                if isinstance(param, IndependentParameter):
                    out[key] = float(vector[i])
        # recompute derived parameters
        out = ParameterSet(
            {
                **out,
                **{
                    key: param.compute(out)
                    for key, param in self.parameters.items()
                    if isinstance(param, DerivedParameter)
                },
            }
        )
        return out

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

        Samples all sampled independent parameters, computes derived values, and
        returns a ``ParameterSet`` ordered canonically.
        """
        # first, sample all independent parameters that are set to be sampled
        p = ParameterSet(
            {
                key: param.sample()
                for key, param in self.parameters.items()
                if isinstance(param, IndependentParameter)
            }
        )
        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
        p = ParameterSet(
            {
                **p,
                **{
                    key: param.compute(p)
                    for key, param in self.parameters.items()
                    if isinstance(param, DerivedParameter)
                },
            }
        )
        logger.debug(
            "[sample_once] Sampled values for all parameters in the bank: %s", p
        )
        # put result in canonical order according to self.canonical_order
        p = self.order(p)
        return p

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

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

        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 ``vector_mode`` is True.

        Returns:
            ParameterSet | pandas.DataFrame | numpy.ndarray: Depending on
            ``vector_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.vector_mode:
                raise ValueError(
                    "Multiple dimensions are only supported for vector mode."
                )
            if len(size) == 1:
                n_samples = size[0]
            else:
                n_samples = int(np.prod(size))

        # print("n_samples (type):", n_samples, type(n_samples))
        # print("size (type):", size, type(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 not param._is_sampled
                        and not isinstance(param, DerivedParameter)
                    },
                }
            )
            samples.append(sample)
        # print("After sampling, there are", len(samples), "samples.")
        if self.vector_mode:
            if size is None:
                out = self.instance_to_vector(samples[0])
            elif isinstance(size, int):
                out = np.array(
                    [self.instance_to_vector(sample) for sample in samples]
                ).reshape((size, len(self.sampled)))
            elif isinstance(size, tuple):
                out = np.array(
                    [self.instance_to_vector(sample) for sample in samples]
                ).reshape(size + (len(self.sampled),))
        else:
            if size is None:
                base = samples[0]
                if with_units:
                    # Apply units on readout for independent parameters that have them,
                    # constructing a new ParameterSet to avoid dtype mutation warnings.
                    data: dict[str, object] = {}
                    for key in base.index:
                        value = base[key]
                        param = self.parameters.get(key)
                        if isinstance(param, (IndependentParameter, DerivedParameter)) and getattr(
                            param, "_unit", None
                        ) is not None:
                            data[key] = value * param._unit
                        else:
                            data[key] = value
                    out = ParameterSet(data)
                else:
                    out = base
            elif isinstance(size, int):
                out = self.instances_to_dataframe([sample for sample in samples])
            elif isinstance(size, tuple):
                out = self.instances_to_dataframe([sample for sample in 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([instance for instance in instances])
        df = df.astype(float)
        return df

    def log_prob(self, input: ParameterSet | pd.DataFrame | np.ndarray) -> float | np.ndarray:
        """Compute a simple log prior for samples under uniform bounds.

        Anything outside the bounds of sampled independent parameters, or
        violating constraints, receives ``-inf``; otherwise ``0``.

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

        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 ``vector_mode`` mode.
        """
        # categorize inputs
        if isinstance(input, ParameterSet):  # if a single sample, package it in a list
            samples = [input]
        elif isinstance(
            input, pd.DataFrame
        ):  # if a DataFrame, convert to list of ParameterSet instances
            samples = [ParameterSet(row) for _, row in input.iterrows()]
        elif isinstance(input, np.ndarray):  # if numpy array ...
            if input.ndim == 1:  # if 1D, treat as a single sample
                if (
                    input.shape[0] != len(self.sampled) and self.vector_mode
                ):  # if vector_mode is enabled, sample must match sampled parameters
                    raise ValueError(
                        f"1D numpy array must have length {len(self.sampled)} to match sampled parameters, since vector_mode is enabled."
                    )
                elif (
                    input.shape[0] != len(self.parameters) and not self.vector_mode
                ):  # if vector_mode is disabled, sample must match all parameters
                    raise ValueError(
                        f"1D numpy array must have length {len(self.parameters)} to match all parameters, since vector_mode is disabled."
                    )
                # print("Converting 1D numpy array to ParameterSet instance.")
                samples = [self.vector_to_instance(input)]  # convert to ParameterSet
            elif input.ndim == 2:  # if 2D, treat each row as a sample
                if input.shape[1] != len(self.sampled) and self.vector_mode:
                    raise ValueError(
                        f"2D numpy array must have {len(self.sampled)} columns to match sampled parameters, since vector_mode is enabled."
                    )
                elif input.shape[1] != len(self.parameters) and not self.vector_mode:
                    raise ValueError(
                        f"2D numpy array must have {len(self.parameters)} columns to match all parameters, since vector_mode is disabled."
                    )
                # print("Converting 2D numpy array to list of ParameterSet instances.")
                samples = [self.vector_to_instance(row) for row in input]
            else:
                raise ValueError("Samples must be a 1D or 2D numpy array.")
        elif not isinstance(input, list):
            raise ValueError(
                "Samples must be a list of ParameterSet instances or a numpy array."
            )

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

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

        Returns 0.0 if within bounds and satisfying constraints, otherwise
        ``-inf``.
        """
        if sample is None or not isinstance(sample, ParameterSet):
            raise ValueError("Sample must be an instance of ParameterSet.")
        result = 0.0
        for key, param in self.parameters.items():
            if isinstance(param, IndependentParameter) and param._is_sampled:
                if not (param.range[0] <= sample[key] <= param.range[1]):
                    result = -np.inf
                    break
                if not all(sample.satisfies(c) for c in self.constraints):
                    result = -np.inf
                    break
        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))
        return out

    def summary(self) -> str:
        """Return a human-readable summary of the bank configuration."""
        lines: list[str] = []
        lines.append("ParameterBank:")
        lines.append("----------------")
        for name, param in self.parameters.items():
            if isinstance(param, IndependentParameter):
                unit = getattr(param, "_unit", None)
                unit_str = f" [{unit}]" if unit is not None else ""
                status = "sampled" if param._is_sampled else "fixed"
                lines.append(
                    f"{name}{unit_str}: {status}, value={param.value}, range={param.range}"
                )
            else:
                lines.append(f"{name}: {param}")
        lines.append("Constraints:")
        lines.append("----------------")
        for constraint in self.constraints:
            lines.append(str(constraint))
        return "\n".join(lines)

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.

lower_bounds: np.ndarray property

Get the lower bounds of all sampled parameters.

upper_bounds: np.ndarray property

Get the upper bounds of all sampled parameters.

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/main.py
558
559
560
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/main.py
562
563
564
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/main.py
566
567
568
def __iter__(self) -> Iterator[str]:
    """Iterate over the parameter names in the bank."""
    return iter(self.parameters)

__getitem__(key: str) -> IndependentParameter | DerivedParameter

Get a parameter by its name.

Source code in jscip/main.py
570
571
572
573
574
575
def __getitem__(self, key: str) -> IndependentParameter | DerivedParameter:
    """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/main.py
577
578
579
580
581
582
583
584
585
586
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(),
        vector_mode=self.vector_mode,
        texnames=self.texnames.copy(),
    )
    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/main.py
594
595
596
597
598
599
600
601
602
603
604
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(key: str, parameter: IndependentParameter | DerivedParameter) -> None

Add a new parameter to the bank.

Source code in jscip/main.py
606
607
608
609
610
611
612
613
614
615
616
617
def add_parameter(
    self, key: str, parameter: IndependentParameter | DerivedParameter
) -> None:
    """Add a new parameter to the bank."""
    if not isinstance(parameter, (IndependentParameter, DerivedParameter)):
        raise ValueError(
            "Parameter must be an instance of IndependentParameter or DerivedParameter."
        )
    if key in self.parameters:
        raise KeyError(f"Parameter '{key}' already exists in the bank.")
    self.parameters[key] = parameter
    logger.debug("Added parameter '%s' to ParameterBank: %s", key, self)

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

Add a new constraint to the bank.

Source code in jscip/main.py
619
620
621
622
623
624
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/main.py
626
627
628
def get_constraints(self) -> list[Callable[[ParameterSet], bool]]:
    """Get all constraints in the bank."""
    return self.constraints

get_default_values(return_vector: bool | None = None, with_units: bool = False) -> 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 (a parameter vector) when return_vector=True.

Parameters:

Name Type Description Default
return_vector 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.vector_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_vector is not a boolean.

Source code in jscip/main.py
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
def get_default_values(self, return_vector: bool | None = None, with_units: bool = False) -> 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 (a
    parameter vector) when ``return_vector=True``.

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

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

    Raises:
        ValueError: If ``return_vector`` is not a boolean.
    """
    if return_vector is None:
        return_vector = (
            self.vector_mode
        )  # default to self.vector_mode if not specified
    if not isinstance(return_vector, bool):
        raise ValueError("return_vector must be a boolean value.")
    p = ParameterSet(
        {
            key: param.value
            for key, param in self.parameters.items()
            if isinstance(param, IndependentParameter)
        }
    )
    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, DerivedParameter)
            },
        }
    )
    p = self.order(p)
    logger.debug(
        "[get_default_values] Default values for all parameters in the bank: %s", p
    )
    if return_vector:
        # Parameter vectors are always returned as plain numeric arrays.
        return self.instance_to_vector(p)
    if with_units:
        # Apply units on readout for independent parameters that have them,
        # constructing a new ParameterSet to avoid dtype mutation warnings.
        data: dict[str, object] = {}
        for key in p.index:
            value = p[key]
            param = self.parameters.get(key)
            if isinstance(param, (IndependentParameter, DerivedParameter)) and getattr(
                param, "_unit", None
            ) is not None:
                data[key] = value * param._unit
            else:
                data[key] = value
        return ParameterSet(data)
    return p

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

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

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.

Raises:

Type Description
ValueError

If input is not a ParameterSet or list thereof.

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

    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.

    Raises:
        ValueError: If ``input`` is not a ``ParameterSet`` or list thereof.
    """
    if not isinstance(input, (ParameterSet, list)):
        raise ValueError(
            "Input must be a ParameterSet or a list of ParameterSet objects."
        )
    if isinstance(input, ParameterSet):
        vector = np.array([input[key] for key in self.sampled])
        logger.debug(
            "[instance_to_vector] Converted ParameterSet to numpy array: %s",
            vector,
        )
    else:
        # return a 2D array of shape (n_instances, n_sampled)
        vector = np.vstack(
            [
                np.array([instance[key] for key in self.sampled])
                for instance in input
            ]
        )
        logger.debug(
            "[instance_to_vector] Converted list of ParameterSet objects to numpy array: %s",
            vector,
        )
    return vector

dataframe_to_vector(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/main.py
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
def dataframe_to_vector(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.")
    vector = df[self.sampled].to_numpy()
    return vector

vector_to_instance(vector: np.ndarray) -> ParameterSet

Convert a parameter vector to a parameter instance.

When vector_mode is True, vector 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
vector 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 vector_mode or if vector is not a NumPy array.

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

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

    Args:
        vector: 1D NumPy array.

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

    Raises:
        ValueError: If shapes are inconsistent with ``vector_mode`` or
            if ``vector`` is not a NumPy array.
    """
    if not isinstance(vector, np.ndarray):
        raise ValueError(
            "Input must be a numpy array, instead got: " + str(type(vector))
        )
    # validate length depending on vector_mode
    if self.vector_mode:
        if len(vector) != len(self.sampled):
            raise ValueError(
                f"Array length {len(vector)} does not match number of sampled parameters {len(self.sampled)}."
            )
    else:
        if len(vector) != len(self.parameters):
            raise ValueError(
                f"Array length {len(vector)} does not match number of parameters {len(self.parameters)}."
            )
    # vector in this case must be a 1D array
    # Start with defaults
    out = self.get_default_values(return_vector=False)
    if self.vector_mode:
        # vector provides only sampled independent parameters
        for i, key in enumerate(self.sampled):
            out[key] = vector[i]
    else:
        # vector provides values for ALL parameters in canonical order
        if len(vector) != len(self.parameters):
            raise ValueError(
                f"Array length {len(vector)} does not match number of parameters {len(self.parameters)}."
            )
        for i, key in enumerate(self.names):
            param = self.parameters[key]
            if isinstance(param, IndependentParameter):
                out[key] = float(vector[i])
    # recompute derived parameters
    out = ParameterSet(
        {
            **out,
            **{
                key: param.compute(out)
                for key, param in self.parameters.items()
                if isinstance(param, DerivedParameter)
            },
        }
    )
    return out

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

Sample parameter sets or parameter vectors.

Parameters:

Name Type Description Default
size int | tuple | None

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

None

Returns:

Type Description
ParameterSet | DataFrame | ndarray

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

ParameterSet | DataFrame | ndarray

vector_mode and size.

Raises:

Type Description
ValueError

If size has an invalid type or dimensionality.

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

    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 ``vector_mode`` is True.

    Returns:
        ParameterSet | pandas.DataFrame | numpy.ndarray: Depending on
        ``vector_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.vector_mode:
            raise ValueError(
                "Multiple dimensions are only supported for vector mode."
            )
        if len(size) == 1:
            n_samples = size[0]
        else:
            n_samples = int(np.prod(size))

    # print("n_samples (type):", n_samples, type(n_samples))
    # print("size (type):", size, type(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 not param._is_sampled
                    and not isinstance(param, DerivedParameter)
                },
            }
        )
        samples.append(sample)
    # print("After sampling, there are", len(samples), "samples.")
    if self.vector_mode:
        if size is None:
            out = self.instance_to_vector(samples[0])
        elif isinstance(size, int):
            out = np.array(
                [self.instance_to_vector(sample) for sample in samples]
            ).reshape((size, len(self.sampled)))
        elif isinstance(size, tuple):
            out = np.array(
                [self.instance_to_vector(sample) for sample in samples]
            ).reshape(size + (len(self.sampled),))
    else:
        if size is None:
            base = samples[0]
            if with_units:
                # Apply units on readout for independent parameters that have them,
                # constructing a new ParameterSet to avoid dtype mutation warnings.
                data: dict[str, object] = {}
                for key in base.index:
                    value = base[key]
                    param = self.parameters.get(key)
                    if isinstance(param, (IndependentParameter, DerivedParameter)) and getattr(
                        param, "_unit", None
                    ) is not None:
                        data[key] = value * param._unit
                    else:
                        data[key] = value
                out = ParameterSet(data)
            else:
                out = base
        elif isinstance(size, int):
            out = self.instances_to_dataframe([sample for sample in samples])
        elif isinstance(size, tuple):
            out = self.instances_to_dataframe([sample for sample in 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/main.py
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
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([instance for instance in instances])
    df = df.astype(float)
    return df

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

Compute a simple log prior for samples under uniform bounds.

Anything outside the bounds of sampled independent parameters, or violating constraints, receives -inf; otherwise 0.

Parameters:

Name Type Description Default
input ParameterSet | DataFrame | ndarray

A ParameterSet, a pandas DataFrame (rows are instances), or a NumPy array (1D or 2D). For arrays, the expected width depends on vector_mode.

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 vector_mode mode.

Source code in jscip/main.py
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
def log_prob(self, input: ParameterSet | pd.DataFrame | np.ndarray) -> float | np.ndarray:
    """Compute a simple log prior for samples under uniform bounds.

    Anything outside the bounds of sampled independent parameters, or
    violating constraints, receives ``-inf``; otherwise ``0``.

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

    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 ``vector_mode`` mode.
    """
    # categorize inputs
    if isinstance(input, ParameterSet):  # if a single sample, package it in a list
        samples = [input]
    elif isinstance(
        input, pd.DataFrame
    ):  # if a DataFrame, convert to list of ParameterSet instances
        samples = [ParameterSet(row) for _, row in input.iterrows()]
    elif isinstance(input, np.ndarray):  # if numpy array ...
        if input.ndim == 1:  # if 1D, treat as a single sample
            if (
                input.shape[0] != len(self.sampled) and self.vector_mode
            ):  # if vector_mode is enabled, sample must match sampled parameters
                raise ValueError(
                    f"1D numpy array must have length {len(self.sampled)} to match sampled parameters, since vector_mode is enabled."
                )
            elif (
                input.shape[0] != len(self.parameters) and not self.vector_mode
            ):  # if vector_mode is disabled, sample must match all parameters
                raise ValueError(
                    f"1D numpy array must have length {len(self.parameters)} to match all parameters, since vector_mode is disabled."
                )
            # print("Converting 1D numpy array to ParameterSet instance.")
            samples = [self.vector_to_instance(input)]  # convert to ParameterSet
        elif input.ndim == 2:  # if 2D, treat each row as a sample
            if input.shape[1] != len(self.sampled) and self.vector_mode:
                raise ValueError(
                    f"2D numpy array must have {len(self.sampled)} columns to match sampled parameters, since vector_mode is enabled."
                )
            elif input.shape[1] != len(self.parameters) and not self.vector_mode:
                raise ValueError(
                    f"2D numpy array must have {len(self.parameters)} columns to match all parameters, since vector_mode is disabled."
                )
            # print("Converting 2D numpy array to list of ParameterSet instances.")
            samples = [self.vector_to_instance(row) for row in input]
        else:
            raise ValueError("Samples must be a 1D or 2D numpy array.")
    elif not isinstance(input, list):
        raise ValueError(
            "Samples must be a list of ParameterSet instances or a numpy array."
        )

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

order(instance: ParameterSet) -> ParameterSet

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

Parameters:

Name Type Description Default
instance ParameterSet

The ParameterSet to reindex.

required

Returns:

Name Type Description
ParameterSet ParameterSet

A new instance with parameters ordered canonically.

Raises:

Type Description
ValueError

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

Source code in jscip/main.py
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
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))
    return out

summary() -> str

Return a human-readable summary of the bank configuration.

Source code in jscip/main.py
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
def summary(self) -> str:
    """Return a human-readable summary of the bank configuration."""
    lines: list[str] = []
    lines.append("ParameterBank:")
    lines.append("----------------")
    for name, param in self.parameters.items():
        if isinstance(param, IndependentParameter):
            unit = getattr(param, "_unit", None)
            unit_str = f" [{unit}]" if unit is not None else ""
            status = "sampled" if param._is_sampled else "fixed"
            lines.append(
                f"{name}{unit_str}: {status}, value={param.value}, range={param.range}"
            )
        else:
            lines.append(f"{name}: {param}")
    lines.append("Constraints:")
    lines.append("----------------")
    for constraint in self.constraints:
        lines.append(str(constraint))
    return "\n".join(lines)

ParameterSet

A single parameter configuration with scalar values.

This is a thin wrapper around pandas.Series used to represent a single instance of parameters, typically produced by sampling a ParameterBank. It preserves the canonical parameter ordering maintained by the bank when reindexed via ParameterBank.order.

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

    This is a thin wrapper around ``pandas.Series`` used to represent a single
    instance of parameters, typically produced by sampling a ``ParameterBank``.
    It preserves the canonical parameter ordering maintained by the bank when
    reindexed via ``ParameterBank.order``.
    """

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

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

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

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

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

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

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

        Returns:
            ParameterSet: A new instance with the same values.
        """
        result = ParameterSet(self.to_dict())
        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/main.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def satisfies(self, constraint: Callable[[ParameterSet], bool]) -> bool:
    """Evaluate a boolean constraint on this instance.

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

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

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

copy() -> ParameterSet

Return a copy of this parameter set.

Returns:

Name Type Description
ParameterSet ParameterSet

A new instance with the same values.

Source code in jscip/main.py
381
382
383
384
385
386
387
388
389
def copy(self) -> ParameterSet:
    """Return a copy of this parameter set.

    Returns:
        ParameterSet: A new instance with the same values.
    """
    result = ParameterSet(self.to_dict())
    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/main.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
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