pysips 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pysips/prior.py ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ Custom Prior Distribution for Unique Random Value Generation.
3
+
4
+ This module provides a specialized prior distribution class that extends the
5
+ ImproperUniform prior from smcpy to generate unique random values using a
6
+ custom generator function. It is designed to prevent duplicate values in
7
+ sampling scenarios where uniqueness is required.
8
+
9
+ Constants
10
+ ---------
11
+ MAX_REPEATS : int
12
+ Maximum number of consecutive attempts allowed before warning about
13
+ potential generator issues (default: 100).
14
+
15
+ Example
16
+ -------
17
+ >>> def my_generator():
18
+ ... return np.random.randint(0, 1000)
19
+ >>>
20
+ >>> prior = Prior(my_generator)
21
+ >>> samples = prior.rvs(10) # Generate 10 unique samples
22
+ >>> print(samples.shape)
23
+ (10, 1)
24
+
25
+ """
26
+
27
+ import warnings
28
+ import numpy as np
29
+
30
+ from smcpy.priors import ImproperUniform
31
+
32
+ MAX_REPEATS = 100
33
+
34
+
35
+ class Prior(ImproperUniform):
36
+ """
37
+ A class that extends ImproperUniform to generate unique random values.
38
+
39
+ This prior uses a custom generator function to produce unique random values
40
+ and warns if the generator repeatedly produces duplicates.
41
+
42
+ Parameters
43
+ ----------
44
+ generator : callable
45
+ A function that generates random values when called with no arguments.
46
+ This generator should return a hashable type.
47
+
48
+ Notes
49
+ -----
50
+ This class tracks duplicate values and warns if the generator fails to
51
+ produce a unique value after a set number of consecutive attempts.
52
+ """
53
+
54
+ def __init__(self, generator):
55
+ super().__init__()
56
+ self._generator = generator
57
+
58
+ # pylint: disable=W0613
59
+ def rvs(self, N, random_state=None):
60
+ """
61
+ Generate N unique random values using the generator.
62
+
63
+ Parameters
64
+ ----------
65
+ N : int
66
+ Number of unique random values to generate.
67
+
68
+ Returns
69
+ -------
70
+ ndarray
71
+ Array of shape (N, 1) containing unique values generated by the generator.
72
+
73
+ Warns
74
+ -----
75
+ UserWarning
76
+ If the generator fails to produce a new unique value after MAX_REPEATS
77
+ consecutive attempts.
78
+
79
+ Notes
80
+ -----
81
+ The random_state parameter is included for compatibility with scipy.stats
82
+ distributions but is not actually used by this method.
83
+ """
84
+ pool = set()
85
+ pool_size = 0
86
+ attempts = 0
87
+ already_warned = False
88
+ while len(pool) < N:
89
+ pool.add(self._generator())
90
+
91
+ if not already_warned:
92
+ if len(pool) == pool_size:
93
+ attempts += 1
94
+ else:
95
+ pool_size = len(pool)
96
+ attempts = 0
97
+
98
+ if attempts >= MAX_REPEATS:
99
+ warnings.warn(
100
+ f"Generator called {MAX_REPEATS} times in a row without finding a "
101
+ "new unique model. This may indicate an issue with the generator "
102
+ "or insufficient unique models available."
103
+ )
104
+ already_warned = True
105
+
106
+ return np.c_[list(pool)]
@@ -0,0 +1,177 @@
1
+ """
2
+ Composite Proposal Generator with Probabilistic Selection.
3
+
4
+ This module provides a meta-proposal mechanism that probabilistically selects
5
+ and applies one or more proposal operators from a collection of available
6
+ proposals. It supports both exclusive selection (choosing exactly one proposal)
7
+ and non-exclusive selection (choosing multiple proposals to apply sequentially).
8
+
9
+ This approach allows for flexible proposal strategies in MCMC sampling or
10
+ evolutionary algorithms by combining different types of modifications (e.g.,
11
+ mutation, crossover, local optimization) with configurable probabilities.
12
+
13
+ Selection Modes
14
+ ---------------
15
+ Exclusive Mode (default)
16
+ Selects exactly one proposal based on the provided probabilities using
17
+ weighted random selection. The probabilities are automatically normalized
18
+ to sum to the cumulative total.
19
+
20
+ Non-Exclusive Mode
21
+ Each proposal is independently selected based on its probability. If no
22
+ proposals are selected in a round, the process repeats until at least one
23
+ is chosen. Selected proposals are applied sequentially in random order.
24
+
25
+ Usage Examples
26
+ --------------
27
+ Exclusive selection (choose one proposal type):
28
+ >>> from mutation import MutationProposal
29
+ >>> from crossover import CrossoverProposal
30
+ >>>
31
+ >>> mutation = MutationProposal(X_dim=3, operators=["+", "*"])
32
+ >>> crossover = CrossoverProposal(gene_pool)
33
+ >>>
34
+ >>> # 70% mutation, 30% crossover
35
+ >>> proposal = RandomChoiceProposal(
36
+ ... [mutation, crossover],
37
+ ... [0.7, 0.3],
38
+ ... exclusive=True
39
+ ... )
40
+
41
+ Non-exclusive selection (can apply multiple proposals):
42
+ >>> # Each proposal has independent 40% chance of being applied
43
+ >>> proposal = RandomChoiceProposal(
44
+ ... [mutation, crossover, local_optimizer],
45
+ ... [0.4, 0.4, 0.2],
46
+ ... exclusive=False
47
+ ... )
48
+
49
+ Integration Notes
50
+ -----------------
51
+ The update() method automatically propagates parameter updates to all
52
+ constituent proposals, making this class compatible with adaptive sampling
53
+ frameworks that modify proposal parameters during execution.
54
+
55
+ All constituent proposals must implement:
56
+ - __call__(model) method for applying the proposal
57
+ - update(*args, **kwargs) method for parameter updates (optional)
58
+ """
59
+
60
+ from bisect import bisect_left
61
+ import numpy as np
62
+
63
+
64
+ class RandomChoiceProposal:
65
+ """Randomly choose a proposal to use
66
+
67
+ Parameters
68
+ ----------
69
+ proposals : list of proposals
70
+ options for the proposal
71
+ probabilities : list of float
72
+ probabilties of choosing each proposal
73
+ exclusive : bool, optional
74
+ whether the proposals are mutually exclusive or if they can all be
75
+ performed at once, by default True
76
+ seed : int, optional
77
+ random seed used to control repeatability
78
+ """
79
+
80
+ def __init__(self, proposals, probabilities, exclusive=True, seed=None):
81
+
82
+ self._proposals = proposals
83
+ self._probabilities = probabilities
84
+ self._cum_probabilities = np.cumsum(probabilities)
85
+ self._exclusive = exclusive
86
+ self._rng = np.random.default_rng(seed)
87
+
88
+ def _select_proposals(self):
89
+ active_proposals = []
90
+
91
+ if self._exclusive:
92
+ rand = self._rng.random() * self._cum_probabilities[-1]
93
+ active_proposals.append(
94
+ self._proposals[bisect_left(self._cum_probabilities, rand)]
95
+ )
96
+ return active_proposals
97
+
98
+ while len(active_proposals) == 0:
99
+ for prop, p in zip(self._proposals, self._probabilities):
100
+ if self._rng.random() < p:
101
+ active_proposals.append(prop)
102
+ self._rng.shuffle(active_proposals)
103
+ return active_proposals
104
+
105
+ def __call__(self, model):
106
+ """
107
+ Apply randomly selected proposal(s) to generate a new model.
108
+
109
+ This method implements the core functionality of the composite proposal
110
+ generator. It selects one or more proposals based on the configured
111
+ probabilities and selection mode, then applies them sequentially to
112
+ transform the input model.
113
+
114
+ Parameters
115
+ ----------
116
+ model : object
117
+ The input model to be transformed. This should be compatible with
118
+ all constituent proposal operators (typically an AGraph for symbolic
119
+ regression or similar structured representation).
120
+
121
+ Returns
122
+ -------
123
+ object
124
+ A new model resulting from applying the selected proposal(s).
125
+ The type matches the input model type.
126
+
127
+ Process Overview
128
+ ----------------
129
+ 1. **Selection Phase**: Randomly selects active proposals based on:
130
+ - Exclusive mode: Exactly one proposal via weighted selection
131
+ - Non-exclusive mode: Zero or more proposals via independent trials
132
+
133
+ 2. **Application Phase**: Applies selected proposals sequentially:
134
+ - First proposal transforms the original model
135
+ - Subsequent proposals transform the result of previous applications
136
+ - Order is randomized in non-exclusive mode to avoid bias
137
+
138
+ Notes
139
+ -----
140
+ - In non-exclusive mode, if no proposals are initially selected, the
141
+ selection process repeats until at least one proposal is chosen
142
+ - Sequential application means later proposals operate on the results
143
+ of earlier ones, potentially creating compound transformations
144
+ """
145
+ active_proposals = self._select_proposals()
146
+ new_model = active_proposals[0](model)
147
+ for prop in active_proposals[1:]:
148
+ new_model = prop(new_model)
149
+ return new_model
150
+
151
+ def update(self, *args, **kwargs):
152
+ """
153
+ Propagate parameter updates to all constituent proposals.
154
+
155
+ This method forwards update calls to all constituent proposal operators,
156
+ enabling the composite proposal to participate in adaptive sampling
157
+ schemes where proposal parameters are modified during the sampling process.
158
+
159
+ Parameters
160
+ ----------
161
+ *args : tuple
162
+ Positional arguments to be passed to each constituent proposal's
163
+ update method. Common examples include new gene pools, population
164
+ statistics, or adaptation parameters.
165
+ **kwargs : dict
166
+ Keyword arguments to be passed to each constituent proposal's
167
+ update method. May include parameters like learning rates,
168
+ temperature schedules, or other adaptive parameters.
169
+
170
+ Returns
171
+ -------
172
+ None
173
+ This method modifies the constituent proposals in-place and does
174
+ not return any values.
175
+ """
176
+ for p in self._proposals:
177
+ p.update(*args, **kwargs)