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/__init__.py +53 -0
- pysips/crossover_proposal.py +138 -0
- pysips/laplace_nmll.py +104 -0
- pysips/metropolis.py +126 -0
- pysips/mutation_proposal.py +220 -0
- pysips/prior.py +106 -0
- pysips/random_choice_proposal.py +177 -0
- pysips/regressor.py +451 -0
- pysips/sampler.py +159 -0
- pysips-0.0.0.dist-info/METADATA +156 -0
- pysips-0.0.0.dist-info/RECORD +26 -0
- pysips-0.0.0.dist-info/WHEEL +5 -0
- pysips-0.0.0.dist-info/licenses/LICENSE +94 -0
- pysips-0.0.0.dist-info/top_level.txt +2 -0
- tests/integration/test_log_likelihood.py +18 -0
- tests/integration/test_prior_with_bingo.py +45 -0
- tests/regression/test_basic_end_to_end.py +131 -0
- tests/regression/test_regressor_end_to_end.py +95 -0
- tests/unit/test_crossover_proposal.py +156 -0
- tests/unit/test_laplace_nmll.py +111 -0
- tests/unit/test_metropolis.py +111 -0
- tests/unit/test_mutation_proposal.py +196 -0
- tests/unit/test_prior.py +135 -0
- tests/unit/test_random_choice_proposal.py +136 -0
- tests/unit/test_regressor.py +227 -0
- tests/unit/test_sampler.py +133 -0
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)
|