Skip to content

Commit 59e06fd

Browse files
Merge pull request #591 from ICB-DCM/develop
Release 0.12.8
2 parents b92b4fb + 98a51b7 commit 59e06fd

File tree

13 files changed

+225
-46
lines changed

13 files changed

+225
-46
lines changed

CHANGELOG.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ Release Notes
88
...........
99

1010

11+
0.12.8 (2022-11-16)
12+
-------------------
13+
14+
* Fix look-ahead implementation in case of biased proposals (#568)
15+
16+
Minor:
17+
18+
* Remove boteh in test env as distributed #7227 got fixed
19+
* Remove obsolete two gaussians test
20+
* Fix Mixin random seed (set it via /dev/urandom)
21+
* Update viserver to bokeh >= 3.0.1 (different import of TabPanel, Tabs)
22+
(all #589)
23+
* Fix sqlalchemy warning "SAWarning: TypeDecorator BytesStorage()
24+
will not produce a cache key" (#590)
25+
26+
1127
0.12.7 (2022-10-30)
1228
-------------------
1329

pyabc/inference/smc.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import copy
44
import logging
55
from datetime import datetime, timedelta
6-
from typing import Callable, List, TypeVar, Union
6+
from typing import Callable, List, Tuple, TypeVar, Union
77

88
import numpy as np
99

@@ -497,7 +497,9 @@ def get_initial_records():
497497
acceptor_config=self.acceptor.get_epsilon_config(t),
498498
)
499499

500-
def _get_initial_population(self, t: int) -> (List[float], List[dict]):
500+
def _get_initial_population(
501+
self, t: int
502+
) -> Tuple[List[float], List[dict]]:
501503
"""
502504
Get initial samples, either from the last population stored in history,
503505
or via sampling sum stats from the prior. This can be used to calibrate

pyabc/inference_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ def evaluate_preliminary_particle(
536536
if acc_res.accept:
537537
weight = sampling_weight * acc_res.weight
538538
else:
539-
weight = 0
539+
weight = 0.0
540540

541541
# return the evaluated particle
542542
return Particle(

pyabc/sampler/eps_mixin.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Client submission interface."""
22

3+
import random
34
from abc import ABC, abstractmethod
45
from time import sleep
56
from typing import Union
@@ -65,6 +66,9 @@ def _full_submit_function_pickle(self, job_id):
6566
# Unpickle function
6667
simulate_one = pickle.loads(self._simulate_accept_one)
6768

69+
random.seed()
70+
np.random.seed()
71+
6872
# Run batch_size evaluations and create list of tuples
6973
result_batch = []
7074
for j in range(self.batch_size):
@@ -91,6 +95,8 @@ def sample_until_n_accepted(
9195
else:
9296
# For advanced pickling, e.g. cloudpickle
9397
def full_submit_function(job_id):
98+
random.seed()
99+
np.random.seed()
94100
# Run batch_size evaluations and create list of tuples
95101
result_batch = []
96102
for j in range(self.batch_size):
@@ -168,6 +174,11 @@ def full_submit_function(job_id):
168174
if result[2] == nth_accepted_id:
169175
break
170176

177+
if sample.n_accepted != n:
178+
raise AssertionError(
179+
f"Got {sample.n_accepted} accepted particles but expected {n}"
180+
)
181+
171182
self.nr_evaluations_ = next_job_id
172183

173184
return sample

pyabc/sampler/mapping.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def map_function(self, simulate_one, _):
8686

8787
np.random.seed()
8888
random.seed()
89+
8990
nr_simulations = 0
9091
sample = self._create_empty_sample()
9192

pyabc/sampler/redis_eps/sampler.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ class RedisEvalParallelSampler(RedisSamplerBase):
181181
lead to a worse performance, especially if evaluation is costly
182182
compared to simulation, because evaluation happens sequentially on the
183183
main thread.
184-
Only effective if `look_ahead=True`.
184+
Only effective if `look_ahead is True`.
185185
max_n_eval_look_ahead_factor:
186186
In delayed evaluation, only this factor times the previous number of
187187
samples are generated, afterwards the workers wait.
@@ -194,6 +194,21 @@ class RedisEvalParallelSampler(RedisSamplerBase):
194194
If not, then the sampler only waits for all simulations that were
195195
started prior to the last started particle of the first `n`
196196
acceptances.
197+
Waiting for all should not be needed, this is for studying purposes.
198+
adapt_look_ahead_proposal:
199+
In look-ahead mode, adapt the preliminary proposal based on previous
200+
acceptances.
201+
In theory, as long as proposal >> prior, everything is fine.
202+
However, in practice, given a finite sample size, in some cases the
203+
preliminary proposal may be biased towards earlier-accepted particles,
204+
which can induce a similar bias in the next accepted population.
205+
Thus, if any parameter dependent simulation time heterogeneity is to be
206+
expected, i.e. if different plausible parameter space regions come
207+
with different simulation times, then this flag should be set to False.
208+
If no such heterogeneity is to be expected, this flag can be set to
209+
True, which can result in improved performance due to a more tailored
210+
proposal distribution.
211+
Only effective if `look_ahead is True`.
197212
log_file:
198213
A file for a dedicated sampler history. Updated in each iteration.
199214
This log file is complementary to the logging realized via the
@@ -210,6 +225,7 @@ def __init__(
210225
look_ahead_delay_evaluation: bool = True,
211226
max_n_eval_look_ahead_factor: float = 10.0,
212227
wait_for_all_samples: bool = False,
228+
adapt_look_ahead_proposal: bool = False,
213229
log_file: str = None,
214230
):
215231
super().__init__(
@@ -220,6 +236,7 @@ def __init__(
220236
self.look_ahead_delay_evaluation: bool = look_ahead_delay_evaluation
221237
self.max_n_eval_look_ahead_factor: float = max_n_eval_look_ahead_factor
222238
self.wait_for_all_samples: bool = wait_for_all_samples
239+
self.adapt_look_ahead_proposal: bool = adapt_look_ahead_proposal
223240

224241
def sample_until_n_accepted(
225242
self,
@@ -546,6 +563,7 @@ def maybe_start_next_generation(
546563
t=t + 1,
547564
population=population,
548565
delay_evaluation=self.look_ahead_delay_evaluation,
566+
adapt_proposal=self.adapt_look_ahead_proposal,
549567
ana_vars=ana_vars,
550568
)
551569

@@ -591,6 +609,12 @@ def create_sample(self, id_results: List[Tuple], n: int) -> Sample:
591609
for j in range(n):
592610
sample += results[j]
593611

612+
# check number of acceptances
613+
if (n_accepted := sample.n_accepted) != n:
614+
raise AssertionError(
615+
f"Expected {n} accepted particles but got {n_accepted}"
616+
)
617+
594618
return sample
595619

596620
def check_analysis_variables(
@@ -604,7 +628,7 @@ def check_analysis_variables(
604628
# nothing to be done
605629
return
606630

607-
def check_bad(var):
631+
def _check_bad(var):
608632
"""Check whether a component is incompatible."""
609633
# do not check for `requires_calibration()`, because in the first
610634
# iteration we do not look ahead
@@ -615,15 +639,16 @@ def check_bad(var):
615639
"sampler's `look_ahead_delay_evaluation` flag."
616640
)
617641

618-
check_bad(acceptor)
619-
check_bad(distance_function)
620-
check_bad(eps)
642+
_check_bad(acceptor)
643+
_check_bad(distance_function)
644+
_check_bad(eps)
621645

622646

623647
def create_preliminary_simulate_one(
624648
t,
625649
population,
626650
delay_evaluation: bool,
651+
adapt_proposal: bool,
627652
ana_vars: AnalysisVars,
628653
) -> Callable:
629654
"""Create a preliminary simulate_one function for generation `t`.
@@ -636,24 +661,36 @@ def create_preliminary_simulate_one(
636661
637662
Parameters
638663
----------
639-
t: The time index for which to create the function (i.e. call with t+1).
640-
population: The preliminary population.
641-
delay_evaluation: Whether to delay evaluation.
642-
ana_vars: The analysis variables.
664+
t:
665+
The time index for which to create the function (i.e. call with t+1).
666+
population:
667+
The preliminary population.
668+
delay_evaluation:
669+
Whether to delay evaluation.
670+
adapt_proposal:
671+
Whether to fit the proposal distribution to the new population.
672+
ana_vars:
673+
The analysis variables.
643674
644675
Returns
645676
-------
646677
simulate_one: The preliminary sampling function.
647678
"""
648679
model_probabilities = population.get_model_probabilities()
649680

650-
# create deep copy of the transition function
651-
transitions = copy.deepcopy(ana_vars.transitions)
652-
653-
# fit transition
654-
for m in population.get_alive_models():
655-
parameters, w = population.get_distribution(m)
656-
transitions[m].fit(parameters, w)
681+
# set proposal distribution
682+
transitions = ana_vars.transitions
683+
if adapt_proposal:
684+
# create deep copy of the transition function
685+
transitions = copy.deepcopy(transitions)
686+
# fit transitions
687+
for m in population.get_alive_models():
688+
parameters, w = population.get_distribution(m)
689+
transitions[m].fit(parameters, w)
690+
elif t == 1:
691+
# at t=0, the prior is used for sampling
692+
# (and the transition not fitted yet)
693+
transitions = ana_vars.parameter_priors
657694

658695
return create_simulate_function(
659696
t=t,
@@ -753,6 +790,13 @@ def post_check_acceptance(
753790
def self_normalize_within_subpopulations(sample: Sample, n: int) -> Sample:
754791
"""Applies subpopulation-wise self-normalization of samples, in-place.
755792
793+
The weights are adjusted per proposal id, such that all particles
794+
belonging to one proposal id have a total weight proportional to the
795+
effective sample size of the sub-population.
796+
This defines the relative importances of all particles in the accepted
797+
population in a reasonabler manner.
798+
Conceptually, also hter normalizations are possible.
799+
756800
Parameters
757801
----------
758802
sample: The population to be returned by the sampler.

pyabc/sampler/redis_eps/server_starter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ def __init__(
109109
batch_size: int = 1,
110110
wait_for_all_samples: bool = False,
111111
look_ahead: bool = False,
112-
look_ahead_delay_evaluation=True,
112+
look_ahead_delay_evaluation: bool = True,
113113
max_n_eval_look_ahead_factor: float = 10.0,
114+
adapt_look_ahead_proposal: bool = False,
114115
workers: int = 2,
115116
processes_per_worker: int = 1,
116117
daemon: bool = True,
@@ -134,6 +135,7 @@ def __init__(
134135
look_ahead=look_ahead,
135136
look_ahead_delay_evaluation=look_ahead_delay_evaluation,
136137
max_n_eval_look_ahead_factor=max_n_eval_look_ahead_factor,
138+
adapt_look_ahead_proposal=adapt_look_ahead_proposal,
137139
log_file=log_file,
138140
)
139141

pyabc/storage/db_model.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,18 @@
3434

3535

3636
class BytesStorage(types.TypeDecorator):
37+
"""Bytes storage.
38+
39+
See https://docs.sqlalchemy.org/en/14/core/custom_types.html.
40+
"""
41+
42+
# Type
3743
impl = LargeBinary
3844

45+
# Safe to be used as part of a cache key, see https://sqlalche.me/e/14/cprf
46+
# (guaranteed to produce the same bind/result behavior every time)
47+
cache_ok = True
48+
3949
def process_bind_param(self, value, dialect): # pylint: disable=R0201
4050
return to_bytes(value)
4151

pyabc/storage/history.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ def get_model_probabilities(
862862
else:
863863
p_models_df = (
864864
pd.DataFrame(p_models, columns=["p", "m", "t"])
865-
.pivot("t", "m", "p")
865+
.pivot(index="t", columns="m", values="p")
866866
.fillna(0)
867867
)
868868
return p_models_df

pyabc/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.12.7'
1+
__version__ = '0.12.8'

0 commit comments

Comments
 (0)