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

mcuntz / partialwrap / 6305437650

25 Sep 2023 10:12PM UTC coverage: 76.174% (-17.1%) from 93.289%
6305437650

push

github

mcuntz
Started moving to reStructuredText and Github pages

14 of 14 new or added lines in 2 files covered. (100.0%)

227 of 298 relevant lines covered (76.17%)

9.14 hits per line

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

55.56
/src/partialwrap/wrappers.py
1
#!/usr/bin/env python
2
"""
12✔
3
Wrappers to partialise functions so that they can be simply called as func(x).
4

5
.. code-block:: python
6

7
    from functools import partial
8
    if isinstance(func, (str,list)):
9
        obj = partial(exe_wrapper, func,
10
                      parameterfile, parameterwriter, outputfile, outputreader,
11
                      {'shell':bool, 'debug':bool, 'pid':bool,
12
                       'pargs':list, 'pkwargs':dict, 'keepparameterfile':bool,
13
                       'oargs':list, 'okwargs':dict, 'keepoutputfile':bool})
14
    else:
15
        obj = partial(function_wrapper, func, arg, kwarg)
16
    fx = obj(x)
17

18
`func` can be a Python function or external executable. External executables
19
will be passed to `subprocess.run(func)`. `func` can then be a string (e.g.
20
'./prog -arg') if `shell=True` or a list (e.g. ['./prog', '-arg']) if
21
`shell=False` (default). Programs without arguments, pipes, etc. can simply be
22
strings with `shell=True` or `False`.
23

24
This module was written by Matthias Cuntz while at Institut National de
25
Recherche pour l'Agriculture, l'Alimentation et l'Environnement (INRAE), Nancy,
26
France.
27

28
Copyright (c) 2016-2022 Matthias Cuntz - mc (at) macu (dot) de
29

30
Released under the MIT License; see LICENSE file for details.
31

32
.. moduleauthor:: Matthias Cuntz
33

34
The following wrappers are provided:
35

36
.. autosummary::
37
   exe_wrapper
38
   exe_mask_wrapper
39
   function_wrapper
40
   function_mask_wrapper
41

42
History
43
    * Written Nov 2016 by Matthias Cuntz (mc (at) macu (dot) de)
44
    * Added x0 and mask to wrapper of external programs,
45
      Jan 2018, Matthias Cuntz
46
    * Added that `pid` is passed to parameterwriter,
47
      and check parameterwriter (getargspec) for number or args, Feb 2018,
48
      Matthias Cuntz
49
    * Removed check of number of parameters of parameterwriter (getargspec) but
50
      add separate wrappers for separate parmeterwriters with different number
51
      or arguments, Feb 2018, Matthias Cuntz
52
    * Added `plotfile` and made docstring sphinx compatible option, Jan 2018,
53
      Matthias Cuntz
54
    * Changed to Sphinx docstring and numpydoc, Nov 2019, Matthias Cuntz
55
    * Remove that `exe_wrappers` support also Python functions. User should use
56
      function_wrappers, Nov 2019, Matthias Cuntz
57
    * Make one `exe_wrapper`, passing bounds, mask, etc. via kwarg dictionary
58
      to parameterwriter; distinguish iterable and array_like parameter types,
59
      Jan 2020, Matthias Cuntz
60
    * Replaced kwarg.pop mechanism because it removed the keywords from
61
      subsequent function calls, Feb 2020, Matthias Cuntz
62
    * Change from ValueError to TypeError if function given to exe wrappers,
63
      Feb 2020, Matthias Cuntz
64
    * Renamed func to function in calling names, objective to output, and
65
      renamed module name to wrappers, May 2020, Matthias Cuntz
66
    * Add arguments for outputreader, its handling of pid, and the ability to
67
      have multiple output files, Jun 2020, Matthias Cuntz
68
    * Correct removal of list of parameterfiles and/or outputfiles, Jun 2020,
69
      Matthias Cuntz
70
    * Added ability to keep produced parameterfiles and outputfiles, Jun 2020,
71
      Matthias Cuntz
72
    * Pass pid just before keyword arguments to parameterwriter and
73
      outputreader, Jun 2020, Matthias Cuntz
74
    * Use `exe_wrapper` in `exe_mask_wrapper`, Jun 2020, Matthias Cuntz
75
    * Make flake8 compliant, Dec 2020, Matthias Cuntz
76
    * Pass exe and pid as list if not shell in exe_wrapper,
77
      May 2021, Matthias Cuntz
78
    * Change to subprocess.run for Python >= v3.5, Aug 2022, Matthias Cuntz
79
    * Added examples and reformatted docstrings, Aug 2022, Matthias Cuntz
80
    * Added error function as fallback option in case subprocess exits with
81
      error code > 0, Aug 2022, Matthias Cuntz
82

83
"""
84
import sys
12✔
85
import subprocess as sp
12✔
86
import os
12✔
87
import numpy as np
12✔
88

89

90
__all__ = ['exe_wrapper', 'exe_mask_wrapper',
12✔
91
           'function_wrapper', 'function_mask_wrapper']
92

93

94
def _tolist(arg):
12✔
95
    """
96
    Assure that *arg* is a list, e.g. if string or None are given.
97

98
    Parameters
99
    ----------
100
    arg :
101
        argument to make list
102

103
    Returns
104
    -------
105
    list
106
        list(arg)
107

108
    Examples
109
    --------
110
    >>> _tolist('string')
111
    ['string']
112
    >>> _tolist([1,2,3])
113
    [1, 2, 3]
114
    >>> _tolist(None)
115
    [None]
116

117
    """
118
    if isinstance(arg, str):
12✔
119
        return [arg]
12✔
120
    try:
×
121
        return list(arg)
×
122
    except TypeError:
×
123
        return [arg]
×
124

125

126
def exe_wrapper(func,
12✔
127
                parameterfile, parameterwriter, outputfile, outputreader,
128
                kwarg, x):
129
    """
130
    Wrapper function for external programs using a *parameterwriter* and
131
    *outputreader* with the interfaces:
132
    ``parameterwriter(parameterfile, x, *pargs, **pkwargs)``
133
    and
134
    ``outputreader(outputfile, *oargs, **okwargs)``
135
    or if *pid==True*:
136
    ``parameterwriter(parameterfile, x, *pargs, pid=pid, **pkwargs)``
137
    and
138
    ``outputreader(outputfile, *oargs, pid=pid, **okwargs)``
139

140
    Examples of *parameterwriter* with *pid==True* are
141
    :any:`standard_parameter_writer` or :any:`sub_params_ja`.
142
    An example of *outputreader* with or without *pid* is
143
    :any:`standard_output_reader`.
144

145
    To be used with :any:`functools.partial`:
146

147
    .. code-block:: python
148

149
       obj = partial(exe_wrapper, func,
150
                     parameterfile, parameterwriter,
151
                     outputfile, outputreader,
152
                     {'shell': bool, 'debug': bool, 'pid': bool,
153
                      'pargs': list, 'pkwargs': dict,
154
                      'keepparameterfile': bool,
155
                      'oargs': list, 'okwargs': dict,
156
                      'keepoutputfile': bool})
157

158
    This allows then calling *obj* simply with the parameters *x*:
159
    ``fx = obj(x)``
160

161
    which translates to:
162

163
    .. code-block:: python
164

165
       parameterwriter(parameterfile, x, *pargs, **pkwargs)
166
       err = subprocess.run(func)
167
       obj = outputreader(outputfile, *oargs, **okwargs)
168

169
    or if *pid==True* to:
170

171
    .. code-block:: python
172

173
       parameterwriter(parameterfile, x, *pargs, pid=pid, **pkwargs)
174
       err = subprocess.run(func)
175
       obj = outputreader(outputfile, *oargs, pid=pid, **okwargs)
176

177
    Note if *pid==True*, it is assumed that *parameterwriter* produces
178
    files in the form `parameterfile + '.' + processid`,
179
    which will be removed after the function call.
180
    Also files in the form `outputfile + '.' + processid`
181
    will be removed automatically.
182

183
    Parameters
184
    ----------
185
    func : string or list of strings
186
        External program to launch by :any:`subprocess`
187
    parameterfile : string or iterable
188
        Filename(s) of parameter file(s)
189
    parameterwriter : callable
190
        Python function writing the *parameterfile*, called as:
191
        ``parameterwriter(parameterfile, x, *pargs, **pkwargs)``
192

193
        or if *pid==True* as:
194
        ``parameterwriter(parameterfile, x, *pargs, pid=pid, **pkwargs)``
195
    outputfile : string or iterable
196
        Filename(s) of file(s) with output values written by the external
197
        executable *func*
198
    outputreader : callable
199
        Python function for reading and processing output value(s) from
200
        *outputfile*, called as:
201
        ``outputreader(ouputfile, x, *oargs, **okwargs)``
202

203
        or if *pid==True* as:
204
        ``outputreader(ouputfile, x, *oargs, pid=pid, **okwargs)``
205
    kwarg : dict
206
        Dictionary with keyword arguments for `exe_wrapper`. Possible
207
        arguments are:
208

209
        * ``shell`` (bool)
210
          If True, :any:`subprocess` opens shell for external executable
211
        * ``debug`` (bool)
212
          If True, model output is displayed while executable is running
213
        * ``pid`` (bool)
214
          If True, append '.RandomNumber' to *parameterfile* and
215
          *outputfile* for parallel calls of *func*
216
        * ``pargs`` (iterable)
217
          List of arguments of `parameterwriter`.
218
        * ``pkwargs`` (dict)
219
          Dictionary with keyword arguments of `parameterwriter`.
220
        * ``keepparameterfile`` (bool)
221
          If True, `parameterfile` produced with `parameterwriter` will
222
          not be deleted after function call.
223
        * ``oargs`` (iterable)
224
          List of arguments of `outputreader`.
225
        * ``okwargs`` (dict)
226
          Dictionary with keyword arguments of `outputreader`.
227
        * ``keepoutputfile`` (bool)
228
          If True, `outputfile` produced by `func` will not be deleted
229
          after function call.
230
        * ``error`` (function)
231
          Function to call in case subprocess exists with error code > 0. The
232
          function should take one argument, which will be the random number if
233
          `pid=True` or *None* otherwise. The return value will be used instead
234
          of the result of *outputreader*.
235

236
    Returns
237
    -------
238
    float
239
        Output value calculated by the external executable *func* or via
240
        the *outputreader*
241

242
    Examples
243
    --------
244
    Imagine an external program *prog.exe* written in C or Fortan, for example,
245
    that reads in two parameters from the file *params.txt* and writes its
246
    results to a file *out.txt*. The parameters can be written with
247
    `standard_parameter_writer` and read with `standard_output_reader`.
248

249
    Then this external program can be wrapped as:
250

251
    >>> from functools import partial
252
    >>> from partialwrap import exe_wrapper, standard_parameter_writer
253
    >>> from partialwrap import standard_output_reader
254
    >>> exe = 'prog.exe'
255
    >>> parameterfile = 'params.txt'
256
    >>> outputfile = 'out.txt'
257
    >>> exewrap = partial(exe_wrapper, exe,
258
    ...                   parameterfile, standard_parameter_writer,
259
    ...                   outputfile, standard_output_reader, {})
260

261
    And is run as:
262

263
    >>> x0 = [0.1, 0.2]
264
    >>> res = exewrap(x0)
265

266
    *res* then holds the output of *prog.exe*.
267

268
    Or one can find the minimum of the function in *prog.exe* with the global
269
    optimizer Differential Evolution (assuming here that *prog.exe* calculates
270
    the Rastrigin function).
271

272
    >>> import scipy.optimize as opt
273
    >>> bounds = [(-5.12, 5.12)] * 2
274
    >>> res = opt.differential_evolution(exewrap, bounds)
275
    >>> res.x
276
    array([-7.32336275e-09,  6.00261842e-09])
277
    >>> res.fun
278
    0.0
279

280
    If *prog.exe* might fail with some parameter combinations from the
281
    Differential Evolution algorithm, then one can give a fallback function for
282
    this cases that, for example, returns arbitrary large values.
283

284
    >>> def err(x):
285
    ...     return np.random.random() * 10000.
286
    >>> exewrap = partial(exe_wrapper, exe,
287
    ...                   parameterfile, standard_parameter_writer,
288
    ...                   outputfile, standard_output_reader,
289
    ...                   {'error': err})
290
    >>> res = opt.differential_evolution(exewrap, bounds)
291

292
    """
293
    if sys.version_info < (3, 7):
12✔
294
        return exe_wrapper_v34(
×
295
            func, parameterfile, parameterwriter, outputfile, outputreader,
296
            kwarg, x)
297
    shell   = kwarg['shell'] if 'shell' in kwarg else False
12✔
298
    debug   = kwarg['debug'] if 'debug' in kwarg else False
12✔
299
    pid     = kwarg['pid'] if 'pid' in kwarg else False
12✔
300
    pargs   = kwarg['pargs'] if 'pargs' in kwarg else []
12✔
301
    pkwargs = kwarg['pkwargs'] if 'pkwargs' in kwarg else {}
12✔
302
    keepparameterfile = (kwarg['keepparameterfile']
12✔
303
                         if 'keepparameterfile' in kwarg else False)
304
    oargs   = kwarg['oargs'] if 'oargs' in kwarg else []
12✔
305
    okwargs = kwarg['okwargs'] if 'okwargs' in kwarg else {}
12✔
306
    keepoutputfile = (kwarg['keepoutputfile']
12✔
307
                      if 'keepoutputfile' in kwarg else False)
308
    errorfunc = kwarg['error'] if 'error' in kwarg else ''
12✔
309
    # For multiprocess but not MPI: pid = mp.current_process()._identity[0]
310
    # seed uses global variables so all processes will produce the same
311
    # random numbers
312
    # -> use np.random.RandomState() for each process to have individual
313
    #    seeds in each process
314
    if pid:
12✔
315
        randst = np.random.RandomState()
12✔
316
        ipid = str(randst.randint(2147483647))
12✔
317
        pipid = '.' + ipid
12✔
318
    else:
319
        ipid = None
12✔
320
        pipid = ''
12✔
321

322
    if isinstance(func, (str, list)):
12✔
323
        if pid:
12✔
324
            parameterwriter(parameterfile, x, *pargs, pid=ipid, **pkwargs)
12✔
325
            if isinstance(func, str):
12✔
326
                if shell:
12✔
327
                    func1 = func + ' ' + ipid
12✔
328
                else:
329
                    func1 = [func, ipid]
×
330
            else:
331
                if shell:
12✔
332
                    func1 = ' '.join(func) + ' ' + ipid
×
333
                else:
334
                    func1 = func + [ipid]
12✔
335
        else:
336
            parameterwriter(parameterfile, x, *pargs, **pkwargs)
12✔
337
            if shell and (not isinstance(func, str)):
12✔
338
                func1 = ' '.join(func)
×
339
            else:
340
                func1 = func
12✔
341

342
        if debug:
12✔
343
            spkwarg = {'stderr': sp.PIPE}
12✔
344
        else:
345
            spkwarg = {'capture_output': True}
12✔
346
        try:
12✔
347
            err = sp.run(func1, check=True, shell=shell, **spkwarg)
12✔
348
            if pid:
12✔
349
                obj = outputreader(outputfile, *oargs, pid=ipid, **okwargs)
12✔
350
            else:
351
                obj = outputreader(outputfile, *oargs, **okwargs)
12✔
352
        except sp.CalledProcessError as e:
12✔
353
            if errorfunc:
12✔
354
                obj = errorfunc(ipid)
12✔
355
            else:
356
                print(e.cmd, 'returned with exit code', e.returncode)
12✔
357
                if not debug:
12✔
358
                    print('stdout was:', e.stdout)
12✔
359
                    print('stderr was:', e.stderr)
12✔
360
                raise ValueError('Subprocess error')
12✔
361

362
        if not keepparameterfile:
12✔
363
            for ff in _tolist(parameterfile):
12✔
364
                if os.path.exists(ff + pipid):
12✔
365
                    os.remove(ff + pipid)
12✔
366

367
        if not keepoutputfile:
12✔
368
            for ff in _tolist(outputfile):
12✔
369
                if os.path.exists(ff + pipid):
12✔
370
                    os.remove(ff + pipid)
12✔
371

372
        return obj
12✔
373
    else:
374
        raise TypeError('func must be string or list of strings for'
12✔
375
                        ' subprocess. Use function_wrapper for Python'
376
                        ' functions.')
377

378

379
def exe_mask_wrapper(func, x0, mask,
12✔
380
                     parameterfile, parameterwriter, outputfile, outputreader,
381
                     kwarg, x):
382
    """
383
    Same as `exe_wrapper` incl./excl. parameters with mask.
384

385
    Makes the transformation:
386

387
    .. code-block:: python
388

389
       xx = np.copy(x0)
390
       xx[mask] = x
391

392
    and calls `exe_wrapper` with *xx*.
393

394
    See `exe_wrapper` for details.
395

396
    Examples
397
    --------
398
    Imagine an external program *prog.exe* written in C or Fortan, for example,
399
    that reads in two parameters from the file *params.txt* and writes its
400
    results to a file *out.txt*. The parameters can be written with
401
    `standard_parameter_writer` and read with `standard_output_reader`.
402

403
    Then this external program can be wrapped as:
404

405
    >>> from functools import partial
406
    >>> from partialwrap import exe_wrapper, standard_parameter_writer
407
    >>> from partialwrap import standard_output_reader
408
    >>> exe = 'prog.exe'
409
    >>> parameterfile = 'params.txt'
410
    >>> outputfile = 'out.txt'
411
    >>> exewrap = partial(exe_wrapper, exe,
412
    ...                   parameterfile, standard_parameter_writer,
413
    ...                   outputfile, standard_output_reader, {})
414

415
    And is run as:
416

417
    >>> x0 = [0.1, 0.2]
418
    >>> res = exewrap(x0)
419

420
    *res* then holds the output of *prog.exe*.
421

422
    Or one can find the minimum of the function in *prog.exe* with the global
423
    optimizer Differential Evolution (assuming here that *prog.exe* calculates
424
    the Rastrigin function).
425

426
    >>> import scipy.optimize as opt
427
    >>> bounds = [(-5.12, 5.12)] * 2
428
    >>> res = opt.differential_evolution(exewrap, bounds)
429
    >>> res.x
430
    array([-7.32336275e-09,  6.00261842e-09])
431
    >>> res.fun
432
    0.0
433

434
    If one wants to fix the second parameter during optimisation, for example
435
    because the parameter is insensitive or one knows the parameter very well,
436
    then this parameter can be excluded from the optimisation by using
437
    `exe_mask_wrapper`, setting the mask to *False*, and providing *initial*
438
    values of the parameters.
439

440
    >>> from partialwrap import exe_mask_wrapper
441
    >>> mask = np.array([True, False])
442
    >>> x0 = np.array([1., 0.])
443
    >>> exemwrap = partial(exe_mask_wrapper, exe, x0, mask,
444
    ...                    parameterfile, standard_parameter_writer,
445
    ...                    outputfile, standard_output_reader, {})
446

447
    >>> bounds = [ bb for ii, bb in enumerate(bounds) if mask[ii] ]
448
    >>> res = opt.differential_evolution(exemwrap, bounds)
449
    >>> res.x
450
    array([-7.32336275e-09])
451
    >>> res.fun
452
    0.0
453

454
    The final output parameters are then:
455

456
    >>> xout = x0.copy()
457
    >>> xout[mask] = res.x
458
    >>> xout
459
    array([-7.32336275e-09,  0.00000000e+00])
460

461
    """
462
    xx = np.copy(x0)
12✔
463
    xx[mask] = x
12✔
464
    return exe_wrapper(func,
12✔
465
                       parameterfile, parameterwriter, outputfile,
466
                       outputreader, kwarg, xx)
467

468

469
def exe_wrapper_v34(func,
12✔
470
                    parameterfile, parameterwriter, outputfile, outputreader,
471
                    kwarg, x):
472
    """
473
    exe_wrapper for Python < v3.7 using subprocess.check_call and
474
    subprocess.check_output
475

476
    See `exe_wrapper` for details.
477

478
    """
479
    shell   = kwarg['shell'] if 'shell' in kwarg else False
×
480
    debug   = kwarg['debug'] if 'debug' in kwarg else False
×
481
    pid     = kwarg['pid'] if 'pid' in kwarg else False
×
482
    pargs   = kwarg['pargs'] if 'pargs' in kwarg else []
×
483
    pkwargs = kwarg['pkwargs'] if 'pkwargs' in kwarg else {}
×
484
    keepparameterfile = (kwarg['keepparameterfile']
×
485
                         if 'keepparameterfile' in kwarg else False)
486
    oargs   = kwarg['oargs'] if 'oargs' in kwarg else []
×
487
    okwargs = kwarg['okwargs'] if 'okwargs' in kwarg else {}
×
488
    keepoutputfile = (kwarg['keepoutputfile']
×
489
                      if 'keepoutputfile' in kwarg else False)
490
    errorfunc = kwarg['error'] if 'error' in kwarg else ''
×
491
    # For multiprocess but not MPI: pid = mp.current_process()._identity[0]
492
    # seed uses global variables so all processes will produce same random
493
    # numbers
494
    # -> use np.random.RandomState() for each processes for individual seeds in
495
    # each process
496
    if pid:
×
497
        randst = np.random.RandomState()
×
498
        ipid   = str(randst.randint(2147483647))
×
499
        pipid = '.' + ipid
×
500
    else:
501
        ipid = None
×
502
        pipid = ''
×
503

504
    if isinstance(func, (str, list)):
×
505
        if pid:
×
506
            parameterwriter(parameterfile, x, *pargs, pid=ipid, **pkwargs)
×
507
            if isinstance(func, str):
×
508
                if shell:
×
509
                    func1 = func + ' ' + ipid
×
510
                else:
511
                    func1 = [func, ipid]
×
512
            else:
513
                func1 = func + [ipid]
×
514
        else:
515
            parameterwriter(parameterfile, x, *pargs, **pkwargs)
×
516
            if shell and (not isinstance(func, str)):
×
517
                func1 = ' '.join(func)
×
518
            else:
519
                func1 = func
×
520

521
        try:
×
522
            if debug:
×
523
                err = sp.check_call(func1, stderr=sp.STDOUT,
×
524
                                    shell=shell)
525
            else:
526
                err = sp.check_output(func1, stderr=sp.STDOUT,
×
527
                                      shell=shell)
528
            if pid:
×
529
                obj = outputreader(outputfile, *oargs, pid=ipid, **okwargs)
×
530
            else:
531
                obj = outputreader(outputfile, *oargs, **okwargs)
×
532
        except sp.CalledProcessError as e:
×
533
            if errorfunc:
×
534
                obj = errorfunc(ipid)
×
535
            else:
536
                print(e.cmd, 'returned with exit code', e.returncode)
×
537
                raise ValueError('Subprocess error')
×
538

539
        if not keepparameterfile:
×
540
            for ff in _tolist(parameterfile):
×
541
                if os.path.exists(ff + pipid):
×
542
                    os.remove(ff + pipid)
×
543

544
        if not keepoutputfile:
×
545
            for ff in _tolist(outputfile):
×
546
                if os.path.exists(ff + pipid):
×
547
                    os.remove(ff + pipid)
×
548

549
        return obj
×
550
    else:
551
        estr  = 'func must be string or list of strings for subprocess.'
×
552
        estr += ' Use function_wrapper for Python functions.'
×
553
        raise TypeError(estr)
×
554

555

556
# Python function wrappers
557
def function_wrapper(func, arg, kwarg, x):
12✔
558
    """
559
    Wrapper for Python functions.
560

561
    To be used with partial:
562
    ``obj = partial(function_wrapper, func, arg, kwarg)``
563

564
    This allows then calling obj with only the non-masked parameters:
565
    ``fx = obj(x)``
566

567
    which translates to:
568
    ``fx = func(x, *arg, **kwarg)``
569

570
    Parameters
571
    ----------
572
    func : callable
573
        Python function to be called `func(x, *arg, **kwarg)`
574
    arg : iterable
575
        Arguments passed to *func*
576
    kwarg : dictionary
577
        Keyword arguments passed to *func*
578

579
    Returns
580
    -------
581
    float
582
        Output value calculated by *func*
583

584
    Examples
585
    --------
586
    Having the Python function *rastrigin* with the parameter *a* and
587
    the keyword argument *b*.
588

589
    >>> import numpy as np
590
    >>> def rastrigin(x, a, b=2.*np.pi):
591
    ...     return a * x.size + np.sum(x**2 - a * np.cos(b * x))
592

593
    We want to evaluate the 5-dimensional rastrigin function at
594
    `[-1, 0, 1, 2, 3]` using the parameters `a=20.`, `b=np.pi`:
595

596
    >>> from functools import partial
597
    >>> from partialwrap import function_wrapper
598
    >>> args   = [20.]
599
    >>> kwargs = {'b': 1.*np.pi}
600
    >>> rastra = partial(function_wrapper, rastrigin, args, kwargs)
601
    >>> x0 = np.array([-1, 0, 1, 2, 3])
602
    >>> res = rastra(x0)
603
    >>> res
604
    135.0
605

606
    Or find its minimum with the global optimizer Differential Evolution,
607
    which allows passing arguments such as `args=(20.)` but does not allow
608
    passing keyword arguments such `b=np.pi`.
609

610
    >>> import scipy.optimize as opt
611
    >>> bounds = [(-5.12, 5.12)] * 5
612
    >>> res = opt.differential_evolution(rastra, bounds)
613
    >>> res.x
614
    array([-7.32336275e-09,  6.00261842e-09,  1.26476721e-09, -1.61965090e-10,
615
           -7.35012009e-09])
616
    >>> res.fun
617
    0.0
618

619
    """
620
    return func(x, *arg, **kwarg)
12✔
621

622

623
def function_mask_wrapper(func, x0, mask, arg, kwarg, x):
12✔
624
    """
625
    Wrapper function for Python function using a mask.
626

627
    To be used with partial:
628
    ``obj = partial(function_mask_wrapper, func, x0, mask, arg, kwarg)``
629

630
    This allows then calling obj with only the non-masked parameters:
631
    ``fx = obj(x)``
632

633
    which translates to:
634

635
    .. code-block:: python
636

637
       xx = np.copy(x0)
638
       xx[mask] = x
639
       fx = func(xx, *arg, **kwarg)
640

641
    Parameters
642
    ----------
643
    func : callable
644
        Python function to be called `func(x, *arg, **kwarg)`
645
    x0 : array_like
646
        Fixed values of masked parameters
647
    mask : array_like
648
        Mask to use *x0* values (`mask[i]=1`) or use new parameters
649
        *x* (`ask[i]=0`) in call of the function
650
    arg : iterable
651
        Arguments passed to *func*
652
    kwarg : dictionary
653
        Keyword arguments passed to *func*
654

655
    Returns
656
    -------
657
    float
658
        Output value calculated by *func*
659

660
    Examples
661
    --------
662
    Having the Python function *rastrigin* with the parameter *a* and
663
    the keyword argument *b*.
664

665
    >>> import numpy as np
666
    >>> def rastrigin(x, a, b=2.*np.pi):
667
    ...     return a * x.size + np.sum(x**2 - a * np.cos(b * x))
668

669
    We want to evaluate the 5-dimensional rastrigin function at
670
    `[-1, 0, 1, 2, 3]` using the parameters `a=20.`, `b=np.pi`:
671

672
    >>> from functools import partial
673
    >>> from partialwrap import function_wrapper
674
    >>> args   = [20.]
675
    >>> kwargs = {'b': 1.*np.pi}
676
    >>> rastra = partial(function_wrapper, rastrigin, args, kwargs)
677
    >>> x0 = np.array([-1, 0, 1, 2, 3])
678
    >>> res = rastra(x0)
679
    >>> res
680
    135.0
681

682
    Or we want find its minimum with the global optimizer Differential
683
    Evolution, which allows passing arguments such as `args=(20.)` but does not
684
    allow passing keyword arguments such `b=np.pi`.
685

686
    >>> import scipy.optimize as opt
687
    >>> bounds = [(-5.12, 5.12)] * 5
688
    >>> res = opt.differential_evolution(rastra, bounds)
689
    >>> res.x
690
    array([-7.32336275e-09,  6.00261842e-09,  1.26476721e-09, -1.61965090e-10,
691
           -7.35012009e-09])
692
    >>> res.fun
693
    0.0
694

695
    If one wants to fix the second parameter during optimisation, for example
696
    because the parameter is insensitive or one knows the parameter very well
697
    (very hypothetical in the example of rastrigin), then this parameter can be
698
    excluded from the optimisation by using `function_mask_wrapper`, setting
699
    the mask to *False*, and providing *initial* values of the parameters.
700

701
    >>> from partialwrap import function_mask_wrapper
702
    >>> mask = np.array([True, False, True, True, True])
703
    >>> x0 = np.array([1., 0., 2., 3., 4.])
704
    >>> rastram = partial(function_mask_wrapper, rastrigin, x0, mask,
705
    ...                   args, kwargs)
706

707
    >>> bounds = [ bb for ii, bb in enumerate(bounds) if mask[ii] ]
708
    >>> res = opt.differential_evolution(rastram, bounds)
709
    >>> res.x
710
    array([-7.32336275e-09,  1.26476721e-09, -1.61965090e-10, -7.35012009e-09])
711
    >>> res.fun
712
    0.0
713

714
    The final output parameters are then:
715

716
    >>> xout = x0.copy()
717
    >>> xout[mask] = res.x
718
    >>> xout
719
    array([-7.32336275e-09,  0.00000000e+00,  1.26476721e-09, -1.61965090e-10,
720
           -7.35012009e-09])
721

722
    """
723
    xx       = np.copy(x0)
12✔
724
    xx[mask] = x
12✔
725
    return func(xx, *arg, **kwarg)
12✔
726

727

728
if __name__ == '__main__':
729
    import doctest
730
    doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)
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

© 2026 Coveralls, Inc