• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

quaquel / EMAworkbench / 5305020467

pending completion
5305020467

Pull #280

github

web-flow
Merge bdfadbcd6 into 83fc014e5
Pull Request #280: bugfix for #277 : load_results properly handles experiments dtypes

2 of 2 new or added lines in 1 file covered. (100.0%)

4617 of 5706 relevant lines covered (80.91%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.73
/ema_workbench/em_framework/samplers.py
1
"""
2

3
This module contains various classes that can be used for specifying different
4
types of samplers. These different samplers implement basic sampling
5
techniques including Full Factorial sampling, Latin Hypercube sampling, and
6
Monte Carlo sampling.
7

8
"""
9
import abc
1✔
10
import itertools
1✔
11
import operator
1✔
12

13
import numpy as np
1✔
14
import scipy.stats as stats
1✔
15

16
from ema_workbench.em_framework import util
1✔
17
from ema_workbench.em_framework.points import Policy, Scenario, Point
1✔
18
from ema_workbench.em_framework.parameters import (
1✔
19
    IntegerParameter,
20
    BooleanParameter,
21
    CategoricalParameter,
22
)
23
from ema_workbench.util.ema_exceptions import EMAError
1✔
24

25
# Created on 16 aug. 2011
26
#
27
# .. codeauthor:: jhkwakkel <j.h.kwakkel (at) tudelft (dot) nl>
28

29
__all__ = [
1✔
30
    "AbstractSampler",
31
    "LHSSampler",
32
    "MonteCarloSampler",
33
    "FullFactorialSampler",
34
    "UniformLHSSampler",
35
    "sample_parameters",
36
    "sample_levers",
37
    "sample_uncertainties",
38
    "determine_parameters",
39
    "DefaultDesigns",
40
]
41

42

43
class AbstractSampler(metaclass=abc.ABCMeta):
1✔
44
    """
45
    Abstract base class from which different samplers can be derived.
46

47
    In the simplest cases, only the sample method needs to be overwritten.
48
    generate_designs` is the only method called from outside. The
49
    other methods are used internally to generate the designs.
50

51
    """
52

53
    def sample(self, distribution, size):
1✔
54
        """
55
        method for sampling a number of samples from a particular distribution.
56
        The various samplers differ with respect to their implementation of
57
        this method.
58

59
        Parameters
60
        ----------
61
        distribution : scipy frozen distribution
62
        size : int
63
               the number of samples to generate
64

65
        Returns
66
        -------
67
        numpy array
68
            the samples for the distribution and specified parameters
69

70
        """
71

72
    def generate_samples(self, parameters, size):
1✔
73
        """
74
        The main method of :class: `~sampler.Sampler` and its
75
        children. This will call the sample method for each of the
76
        parameters and return the resulting designs.
77

78
        Parameters
79
        ----------
80
        parameters : collection
81
                     a collection of :class:`~parameters.RealParameter`,
82
                     :class:`~parameters.IntegerParameter`,
83
                     and :class:`~parameters.CategoricalParameter`
84
                     instances.
85
        size : int
86
               the number of samples to generate.
87

88

89
        Returns
90
        -------
91
        dict
92
            dict with the parameter.name as key, and the sample as value
93

94
        """
95
        return {param.name: self.sample(param.dist, size) for param in parameters}
1✔
96

97
    def generate_designs(self, parameters, nr_samples):
1✔
98
        """external interface for Sampler. Returns the computational experiments
99
        over the specified parameters, for the given number of samples for each
100
        parameter.
101

102
        Parameters
103
        ----------
104
        parameters : list
105
                        a list of parameters for which to generate the
106
                        experimental designs
107
        nr_samples : int
108
                     the number of samples to draw for each parameter
109

110

111
        Returns
112
        -------
113
        generator
114
            a generator object that yields the designs resulting from
115
            combining the parameters
116
        int
117
            the number of experimental designs
118

119
        """
120
        parameters = sorted(parameters, key=operator.attrgetter("name"))
1✔
121
        sampled_parameters = self.generate_samples(parameters, nr_samples)
1✔
122
        designs = zip(*[sampled_parameters[u.name] for u in parameters])
1✔
123
        designs = DefaultDesigns(designs, parameters, nr_samples)
1✔
124

125
        return designs
1✔
126

127

128
class LHSSampler(AbstractSampler):
1✔
129
    """
130
    generates a Latin Hypercube sample for each of the parameters
131
    """
132

133
    def sample(self, distribution, size):
1✔
134
        """
135
        generate a Latin Hypercube Sample.
136

137
        Parameters
138
        ----------
139
        distribution : scipy frozen distribution
140
        size : int
141
               the number of samples to generate
142

143
        Returns
144
        -------
145
        dict
146
            with the paramertainty.name as key, and the sample as value
147

148
        """
149

150
        perc = np.arange(0, 1.0, 1.0 / size)
1✔
151
        np.random.shuffle(perc)
1✔
152
        smp = stats.uniform(perc, 1.0 / size).rvs()
1✔
153
        samples = distribution.ppf(smp)
1✔
154

155
        # TODO::
156
        # corner case fix (try siz=49)
157
        # is not a proper fix, it means that perc is wrong
158
        # so your intervals are wrong
159
        samples = samples[np.isnan(samples) == False]
1✔
160

161
        return samples
1✔
162

163

164
class UniformLHSSampler(LHSSampler):
1✔
165
    def generate_samples(self, parameters, size):
1✔
166
        """
167

168
        Parameters
169
        ----------
170
        parameters : collection
171
        size : int
172

173
        Returns
174
        -------
175
        dict
176
            dict with the parameter.name as key, and the sample as value
177

178
        """
179

180
        samples = {}
×
181
        for param in parameters:
×
182
            lower_bound = param.lower_bound
×
183
            upper_bound = param.upper_bound
×
184

185
            if isinstance(param.dist, stats.rv_continuous):
×
186
                dist = stats.uniform(lower_bound, upper_bound - lower_bound)
×
187
            else:
188
                dist = stats.randint(lower_bound, upper_bound + 1)
×
189
            samples[param.name] = self.sample(dist, size)
×
190
        return samples
×
191

192

193
# class FactorialLHSSampler(LHSSampler):
194
#     """generate LHS samples over the well characterized and the deeply
195
#     uncertain factors separately, and than combine them in a full factorial
196
#     way
197
#
198
#     Parameters
199
#     ----------
200
#     n_uniform : int
201
#                 the number of samples for the deeply uncertain factor
202
#     n_informative : int
203
#                     the number of samples for the well characterized uncertain
204
#                     factors
205
#
206
#     TODO:: needs a better name
207
#     """
208
#
209
#     def __init__(self, n_uniform, n_informative):
210
#         LHSSampler.__init__(self)
211
#
212
#     def generate_designs(self, parameters, nr_samples):
213
#         """
214
#
215
#         Parameters
216
#         ----------
217
#         parameters : list
218
#         nr_samples : int
219
#
220
#         Returns
221
#         -------
222
#         generator
223
#         int
224
#
225
#         """
226
#         parameters = sorted(parameters, key=operator.attrgetter('name'))
227
#
228
#         deeply_uncertain_parameters = []
229
#         well_characterized_parameters = []
230
#         for parameter in parameters:
231
#             if isinstance(parameter.dist, (stats.randint, stats.uniform)):
232
#                 deeply_uncertain_parameters.append(parameter)
233
#             else:
234
#                 well_characterized_parameters.append(parameter)
235
#
236
#         raise NotImplementedError
237
#         # code below here makes no sense
238
#         sampled_parameters = self.generate_samples(parameters, nr_samples)
239
#         designs = zip(*[sampled_parameters[u.name] for u in parameters])
240
#         designs = DefaultDesigns(designs, parameters, nr_samples)
241
#
242
#         return designs
243

244

245
class MonteCarloSampler(AbstractSampler):
1✔
246
    """
247
    generates a Monte Carlo sample for each of the parameters.
248

249
    """
250

251
    def sample(self, distribution, size):
1✔
252
        """
253
        generate a Monte Carlo Sample.
254

255
        Parameters
256
        ----------
257
        distribution : scipy frozen distribution
258
        size : int
259
               the number of samples to generate
260

261
        Returns
262
        -------
263
        dict
264
            with the paramertainty.name as key, and the sample as value
265

266
        """
267

268
        return distribution.rvs(size)
1✔
269

270

271
class FullFactorialSampler(AbstractSampler):
1✔
272
    """
273
    generates a full factorial sample.
274

275
    If the parameter is non categorical, the resolution is set the
276
    number of samples. If the parameter is categorical, the specified value
277
    for samples will be ignored and each category will be used instead.
278

279
    """
280

281
    def generate_samples(self, parameters, size):
1✔
282
        """
283
        The main method of :class: `~sampler.Sampler` and its
284
        children. This will call the sample method for each of the
285
        parameters and return the resulting samples
286

287
        Parameters
288
        ----------
289
        parameters : collection
290
                        a collection of :class:`~parameters.Parameter`
291
                        instances
292
        size : int
293
                the number of samples to generate.
294

295
        Returns
296
        -------
297
        dict
298
            with the paramertainty.name as key, and the sample as value
299
        """
300
        samples = {}
1✔
301
        for param in parameters:
1✔
302
            cats = param.resolution
1✔
303
            if not cats:
1✔
304
                cats = np.linspace(param.lower_bound, param.upper_bound, size)
1✔
305
                if isinstance(param, IntegerParameter):
1✔
306
                    cats = np.round(cats, 0)
1✔
307
                    cats = set(cats)
1✔
308
                    cats = (int(entry) for entry in cats)
1✔
309
                    cats = sorted(cats)
1✔
310
            samples[param.name] = cats
1✔
311

312
        return samples
1✔
313

314
    def generate_designs(self, parameters, nr_samples):
1✔
315
        """
316
        This method provides an alternative implementation to the default
317
        implementation provided by :class:`~sampler.Sampler`. This
318
        version returns a full factorial design across the parameters.
319

320
        Parameters
321
        ----------
322
        parameters : list
323
                        a list of parameters for which to generate the
324
                        experimental designs
325
        nr_samples : int
326
                     the number of intervals to use on each
327
                     Parameter. Categorical parameters always
328
                     return all their categories
329

330
        Returns
331
        -------
332
        generator
333
            a generator object that yields the designs resulting from
334
            combining the parameters
335
        int
336
            the number of experimental designs
337

338
        """
339
        parameters = sorted(parameters, key=operator.attrgetter("name"))
1✔
340

341
        samples = self.generate_samples(parameters, nr_samples)
1✔
342
        zipped_samples = itertools.product(*[samples[u.name] for u in parameters])
1✔
343

344
        n_designs = self.determine_nr_of_designs(samples)
1✔
345
        designs = DefaultDesigns(zipped_samples, parameters, n_designs)
1✔
346

347
        return designs
1✔
348

349
    def determine_nr_of_designs(self, sampled_parameters):
1✔
350
        """
351
        Helper function for determining the number of experiments that will
352
        be generated given the sampled parameters.
353

354
        Parameters
355
        ----------
356
        sampled_parameters : list
357
                        a list of sampled parameters, as
358
                        the values return by generate_samples
359

360
        Returns
361
        -------
362
        int
363
            the total number of experimental design
364
        """
365
        nr_designs = 1
1✔
366
        for value in sampled_parameters.values():
1✔
367
            nr_designs *= len(value)
1✔
368
        return nr_designs
1✔
369

370

371
# class PartialFactorialSampler(AbstractSampler):
372
#     """
373
#     generates a partial factorial design over the parameters. Any parameter
374
#     where factorial is true will be included in a factorial design, while the
375
#     remainder will be sampled using LHS or MC sampling.
376
#
377
#     Parameters
378
#     ----------
379
#     sampling: {PartialFactorialSampler.LHS, PartialFactorialSampler.MC}, optional
380
#               the desired sampling for the non factorial parameters.
381
#
382
#     Raises
383
#     ------
384
#     ValueError
385
#         if sampling is not either LHS or MC
386
#
387
#     """
388
#
389
#     LHS = 'LHS'
390
#     MC = 'MC'
391
#
392
#     def __init__(self, sampling='LHS'):
393
#         super(PartialFactorialSampler, self).__init__()
394
#
395
#         if sampling == PartialFactorialSampler.LHS:
396
#             self.sampler = LHSSampler()
397
#         elif sampling == PartialFactorialSampler.MC:
398
#             self.sampler = MonteCarloSampler()
399
#         else:
400
#             raise ValueError(('invalid value for sampling type, should be LHS '
401
#                               'or MC'))
402
#         self.ff = FullFactorialSampler()
403
#
404
#     def _sort_parameters(self, parameters):
405
#         """sort parameters into full factorial and other
406
#
407
#         Parameters
408
#         ----------
409
#         parameters : list of parameters
410
#
411
#         """
412
#         ff_params = []
413
#         other_params = []
414
#
415
#         for param in parameters:
416
#             if param.pff:
417
#                 ff_params.append(param)
418
#             else:
419
#                 other_params.append(param)
420
#
421
#         if not ff_params:
422
#             raise EMAError("no parameters for full factorial sampling")
423
#         if not other_params:
424
#             raise EMAError("no parameters for normal sampling")
425
#
426
#         return ff_params, other_params
427
#
428
#     def generate_designs(self, parameters, nr_samples):
429
#         """external interface to sampler. Returns the computational experiments
430
#         over the specified parameters, for the given number of samples for each
431
#         parameter.
432
#
433
#         Parameters
434
#         ----------
435
#         parameters : list
436
#                         a list of parameters for which to generate the
437
#                         experimental designs
438
#         nr_samples : int
439
#                      the number of samples to draw for each parameter
440
#
441
#         Returns
442
#         -------
443
#         generator
444
#             a generator object that yields the designs resulting from
445
#             combining the parameters
446
#         int
447
#             the number of experimental designs
448
#
449
#         """
450
#
451
#         ff_params, other_params = self._sort_parameters(parameters)
452
#
453
#         # generate a design over the factorials
454
#         # TODO update ff to use resolution if present
455
#         ff_designs = self.ff.generate_designs(ff_params, nr_samples)
456
#
457
#         # generate a design over the remainder
458
#         # for each factorial, run the MC design
459
#         other_designs = self.sampler.generate_designs(other_params,
460
#                                                       nr_samples)
461
#
462
#         nr_designs = other_designs.n * ff_designs.n
463
#
464
#         designs = PartialFactorialDesigns(ff_designs, other_designs,
465
#                                           ff_params + other_params, nr_designs)
466
#
467
#         return designs
468

469

470
def determine_parameters(models, attribute, union=True):
1✔
471
    """determine the parameters over which to sample
472

473
    Parameters
474
    ----------
475
    models : a collection of AbstractModel instances
476
    attribute : {'uncertainties', 'levers'}
477
    union : bool, optional
478
            in case of multiple models, sample over the union of
479
            levers, or over the intersection of the levers
480

481
    Returns
482
    -------
483
    collection of Parameter instances
484

485
    """
486
    return util.determine_objects(models, attribute, union=union)
1✔
487

488

489
def sample_parameters(parameters, n_samples, sampler=LHSSampler(), kind=Point):
1✔
490
    """generate cases by sampling over the parameters
491

492
    Parameters
493
    ----------
494
    parameters : collection of AbstractParameter instances
495
    n_samples : int
496
    sampler : Sampler instance, optional
497
    kind : {Case, Scenario, Policy}, optional
498
            the class into which the samples are collected
499

500
    Returns
501
    -------
502
    generator yielding Case, Scenario, or Policy instances
503

504
    """
505

506
    samples = sampler.generate_designs(parameters, n_samples)
×
507
    samples.kind = kind
×
508

509
    return samples
×
510

511

512
def sample_levers(models, n_samples, union=True, sampler=LHSSampler()):
1✔
513
    """generate policies by sampling over the levers
514

515
    Parameters
516
    ----------
517
    models : a collection of AbstractModel instances
518
    n_samples : int
519
    union : bool, optional
520
            in case of multiple models, sample over the union of
521
            levers, or over the intersection of the levers
522
    sampler : Sampler instance, optional
523

524
    Returns
525
    -------
526
    generator yielding Policy instances
527

528
    """
529
    levers = determine_parameters(models, "levers", union=union)
×
530

531
    if not levers:
×
532
        raise EMAError("you are trying to sample policies, but no levers have been defined")
×
533

534
    return sample_parameters(levers, n_samples, sampler, Policy)
×
535

536

537
def sample_uncertainties(models, n_samples, union=True, sampler=LHSSampler()):
1✔
538
    """generate scenarios by sampling over the uncertainties
539

540
    Parameters
541
    ----------
542
    models : a collection of AbstractModel instances
543
    n_samples : int
544
    union : bool, optional
545
            in case of multiple models, sample over the union of
546
            uncertainties, or over the intersection of the uncertainties
547
    sampler : Sampler instance, optional
548

549
    Returns
550
    -------
551
    generator yielding Scenario instances
552

553
    """
554
    uncertainties = determine_parameters(models, "uncertainties", union=union)
×
555

556
    if not uncertainties:
×
557
        raise EMAError("you are trying to sample scenarios, but no uncertainties have been defined")
×
558

559
    return sample_parameters(uncertainties, n_samples, sampler, Policy)
×
560

561

562
# def sample_jointly(models, n_samples, uncertainty_union=True, lever_union=True,
563
#                    sampler=LHSSampler()):
564
#     """generate scenarios by sampling over the uncertainties
565
#
566
#     Parameters
567
#     ----------
568
#     models : a collection of AbstractModel instances
569
#     n_samples : int
570
#     uncertainty_union : bool, optional
571
#             in case of multiple models, sample over the union of
572
#             uncertainties, or over the intersection of the uncertainties
573
#     lever_union : bool, optional
574
#             in case of multiple models, sample over the union of
575
#             levers, or over the intersection of the levers
576
#     sampler : Sampler instance, optional
577
#
578
#     Returns
579
#     -------
580
#     generator
581
#         yielding Scenario instances
582
#     collection
583
#         the collection of parameters over which to sample
584
#     n_samples
585
#         the number of designs
586
#     """
587
#     uncertainties = determine_parameters(models, 'uncertainties',
588
#                                          union=uncertainty_union)
589
#     levers = determine_parameters(models, 'levers', union=lever_union)
590
#     parameters = uncertainties.copy() + levers.copy()
591
#
592
#     samples = sampler.generate_designs(parameters, n_samples)
593
#     samples.kind = Scenario
594
#
595
#     return samples
596

597

598
def from_experiments(models, experiments):
1✔
599
    """generate scenarios from an existing experiments DataFrame
600

601
    Parameters
602
    ----------
603
    models : collection of AbstractModel instances
604
    experiments : DataFrame
605

606
    Returns
607
    -------
608
     generator
609
        yielding Scenario instances
610

611
    """
612
    policy_names = np.unique(experiments["policy"])
×
613
    model_names = np.unique(experiments["model"])
×
614

615
    # we sample ff over models and policies so we need to ensure
616
    # we only get the experiments for a single model policy combination
617
    logical = (experiments["model"] == model_names[0]) & (experiments["policy"] == policy_names[0])
×
618

619
    experiments = experiments[logical]
×
620

621
    uncertainties = util.determine_objects(models, "uncertainties", union=True)
×
622
    samples = {unc.name: experiments[:, unc.name] for unc in uncertainties}
×
623

624
    scenarios = DefaultDesigns(samples, uncertainties, experiments.shape[0])
×
625
    scenarios.kind = Scenario
×
626

627
    return scenarios
×
628

629

630
class DefaultDesigns:
1✔
631
    """iterable for the experimental designs"""
632

633
    def __init__(self, designs, parameters, n):
1✔
634
        self.designs = list(designs)
1✔
635
        self.parameters = parameters
1✔
636
        self.params = [p.name for p in parameters]
1✔
637
        self.kind = None
1✔
638
        self.n = n
1✔
639

640
    @abc.abstractmethod
1✔
641
    def __iter__(self):
1✔
642
        """should return iterator"""
643

644
        return design_generator(self.designs, self.parameters, self.kind)
1✔
645

646
    def __str__(self):
1✔
647
        return ("ema_workbench.DefaultDesigns, " "{} designs on {} parameters").format(
×
648
            self.n, len(self.params)
649
        )
650

651

652
# class PartialFactorialDesigns(object):
653
#
654
#     @property
655
#     def kind(self):
656
#         return self._kind
657
#
658
#     @kind.setter
659
#     def kind(self, value):
660
#         self._kind = value
661
#         self.ff_designs.kind = value
662
#         self.other_designs.kind = value
663
#
664
#     def __init__(self, ff_designs, other_designs, parameters, n):
665
#         self.ff_designs = ff_designs
666
#         self.other_designs = other_designs
667
#
668
#         self.parameters = parameters
669
#         self.params = [p.name for p in parameters]
670
#
671
#         self._kind = None
672
#         self.n = n
673
#
674
#     def __iter__(self):
675
#         designs = itertools.product(self.ff_designs, self.other_designs)
676
#         return partial_designs_generator(designs)
677

678

679
# def partial_designs_generator(designs):
680
#     """generator which combines the full factorial part of the design
681
#     with the non full factorial part into a single dict
682
#
683
#     Parameters
684
#     ----------
685
#     designs: iterable of tuples
686
#
687
#     Yields
688
#     ------
689
#     dict
690
#         experimental design dict
691
#
692
#     """
693
#
694
#     for design in designs:
695
#         try:
696
#             ff_part, other_part = design
697
#         except ValueError:
698
#             ff_part = design
699
#             other_part = {}
700
#
701
#         design = ff_part.copy()
702
#         design.update(other_part)
703
#
704
#         yield design
705

706

707
def design_generator(designs, params, kind):
1✔
708
    """generator that combines the sampled parameters with their correct
709
    name in order to return dicts.
710

711
    Parameters
712
    ----------
713
    designs : iterable of tuples
714
    params : iterable of str
715
    kind : cls
716

717
    Yields
718
    ------
719
    dict
720
        experimental design dictionary
721

722
    """
723

724
    for design in designs:
1✔
725
        design_dict = {}
1✔
726
        for param, value in zip(params, design):
1✔
727
            if isinstance(param, IntegerParameter):
1✔
728
                value = int(value)
1✔
729
            if isinstance(param, BooleanParameter):
1✔
730
                value = bool(value)
×
731
            if isinstance(param, CategoricalParameter):
1✔
732
                # categorical parameter is an integer parameter, so
733
                # conversion to int is already done
734
                value = param.cat_for_index(value).value
1✔
735

736
            design_dict[param.name] = value
1✔
737

738
        yield kind(**design_dict)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc