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

python-control / python-control / 16089114428

05 Jul 2025 02:35PM UTC coverage: 94.734% (+0.001%) from 94.733%
16089114428

Pull #1164

github

web-flow
Merge 3993c7952 into 03ae372c1
Pull Request #1164: OS/BLAS update for Windows + small fixes for 0.10.2 release

9949 of 10502 relevant lines covered (94.73%)

8.28 hits per line

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

92.53
control/config.py
1
# config.py - package defaults
2
# RMM, 4 Nov 2012
3
#
4
# TODO: add ability to read/write configuration files (a la matplotlib)
5

6
"""Functions to access default parameter values.
7

8
This module contains default values and utility functions for setting
9
parameters that control the behavior of the control package.
10

11
"""
12

13
import collections
9✔
14
import warnings
9✔
15

16
from .exception import ControlArgument
9✔
17

18
__all__ = ['defaults', 'set_defaults', 'reset_defaults',
9✔
19
           'use_matlab_defaults', 'use_fbs_defaults',
20
           'use_legacy_defaults']
21

22
# Package level default values
23
_control_defaults = {
9✔
24
    'control.default_dt': 0,
25
    'control.squeeze_frequency_response': None,
26
    'control.squeeze_time_response': None,
27
    'forced_response.return_x': False,
28
}
29

30

31
class DefaultDict(collections.UserDict):
9✔
32
    """Default parameters dictionary, with legacy warnings.
33

34
    If a user wants to write to an old setting, issue a warning and write to
35
    the renamed setting instead. Accessing the old setting returns the value
36
    from the new name.
37
    """
38

39
    def __init__(self, *args, **kwargs):
9✔
40
        super().__init__(*args, **kwargs)
9✔
41

42
    def __setitem__(self, key, value):
9✔
43
        super().__setitem__(self._check_deprecation(key), value)
9✔
44

45
    def __missing__(self, key):
9✔
46
        # An old key should never have been set. If it is being accessed
47
        # through __getitem__, return the value from the new name.
48
        repl = self._check_deprecation(key)
9✔
49
        if self.__contains__(repl):
9✔
50
            return self[repl]
9✔
51
        else:
52
            raise KeyError(key)
9✔
53

54
    # New get function for Python 3.12+ to replicate old behavior
55
    def get(self, key, defval=None):
9✔
56
        # If the key exists, return it
57
        if self.__contains__(key):
9✔
58
            return self[key]
9✔
59

60
        # If not, see if it is deprecated
61
        repl = self._check_deprecation(key)
9✔
62
        if self.__contains__(repl):
9✔
63
            return self.get(repl, defval)
9✔
64

65
        # Otherwise, call the usual dict.get() method
66
        return super().get(key, defval)
9✔
67

68
    def _check_deprecation(self, key):
9✔
69
        if self.__contains__(f"deprecated.{key}"):
9✔
70
            repl = self[f"deprecated.{key}"]
9✔
71
            warnings.warn(f"config.defaults['{key}'] has been renamed to "
9✔
72
                          f"config.defaults['{repl}'].",
73
                          FutureWarning, stacklevel=3)
74
            return repl
9✔
75
        else:
76
            return key
9✔
77

78
    #
79
    # Context manager functionality
80
    #
81

82
    def __call__(self, mapping):
9✔
83
        self.saved_mapping = dict()
9✔
84
        self.temp_mapping = mapping.copy()
9✔
85
        return self
9✔
86

87
    def __enter__(self):
9✔
88
        for key, val in self.temp_mapping.items():
9✔
89
            if not key in self:
9✔
90
                raise ValueError(f"unknown parameter '{key}'")
9✔
91
            self.saved_mapping[key] = self[key]
9✔
92
            self[key] = val
9✔
93
        return self
9✔
94

95
    def __exit__(self, exc_type, exc_val, exc_tb):
9✔
96
        for key, val in self.saved_mapping.items():
9✔
97
            self[key] = val
9✔
98
        del self.saved_mapping, self.temp_mapping
9✔
99
        return None
9✔
100

101
defaults = DefaultDict(_control_defaults)
9✔
102

103

104
def set_defaults(module, **keywords):
9✔
105
    """Set default values of parameters for a module.
106

107
    The set_defaults() function can be used to modify multiple parameter
108
    values for a module at the same time, using keyword arguments.
109

110
    Parameters
111
    ----------
112
    module : str
113
        Name of the module for which the defaults are being given.
114
    **keywords : keyword arguments
115
        Parameter value assignments.
116

117
    Examples
118
    --------
119
    >>> ct.defaults['freqplot.number_of_samples']
120
    1000
121
    >>> ct.set_defaults('freqplot', number_of_samples=100)
122
    >>> ct.defaults['freqplot.number_of_samples']
123
    100
124
    >>> # do some customized freqplotting
125

126
    """
127
    if not isinstance(module, str):
9✔
128
        raise ValueError("module must be a string")
×
129
    for key, val in keywords.items():
9✔
130
        keyname = module + '.' + key
9✔
131
        if keyname not in defaults and f"deprecated.{keyname}" not in defaults:
9✔
132
            raise TypeError(f"unrecognized keyword: {key}")
9✔
133
        defaults[module + '.' + key] = val
9✔
134

135

136
# TODO: allow individual modules and individual parameters to be reset
137
def reset_defaults():
9✔
138
    """Reset configuration values to their default (initial) values.
139

140
    Examples
141
    --------
142
    >>> ct.defaults['freqplot.number_of_samples']
143
    1000
144
    >>> ct.set_defaults('freqplot', number_of_samples=100)
145
    >>> ct.defaults['freqplot.number_of_samples']
146
    100
147

148
    >>> # do some customized freqplotting
149
    >>> ct.reset_defaults()
150
    >>> ct.defaults['freqplot.number_of_samples']
151
    1000
152

153
    """
154
    # System level defaults
155
    defaults.update(_control_defaults)
9✔
156

157
    from .ctrlplot import _ctrlplot_defaults, reset_rcParams
9✔
158
    reset_rcParams()
9✔
159
    defaults.update(_ctrlplot_defaults)
9✔
160

161
    from .freqplot import _freqplot_defaults, _nyquist_defaults
9✔
162
    defaults.update(_freqplot_defaults)
9✔
163
    defaults.update(_nyquist_defaults)
9✔
164

165
    from .nichols import _nichols_defaults
9✔
166
    defaults.update(_nichols_defaults)
9✔
167

168
    from .pzmap import _pzmap_defaults
9✔
169
    defaults.update(_pzmap_defaults)
9✔
170

171
    from .rlocus import _rlocus_defaults
9✔
172
    defaults.update(_rlocus_defaults)
9✔
173

174
    from .sisotool import _sisotool_defaults
9✔
175
    defaults.update(_sisotool_defaults)
9✔
176

177
    from .iosys import _iosys_defaults
9✔
178
    defaults.update(_iosys_defaults)
9✔
179

180
    from .xferfcn import _xferfcn_defaults
9✔
181
    defaults.update(_xferfcn_defaults)
9✔
182

183
    from .statesp import _statesp_defaults
9✔
184
    defaults.update(_statesp_defaults)
9✔
185

186
    from .optimal import _optimal_defaults
9✔
187
    defaults.update(_optimal_defaults)
9✔
188

189
    from .timeplot import _timeplot_defaults
9✔
190
    defaults.update(_timeplot_defaults)
9✔
191

192
    from .phaseplot import _phaseplot_defaults
9✔
193
    defaults.update(_phaseplot_defaults)
9✔
194

195

196
def _get_param(module, param, argval=None, defval=None, pop=False, last=False):
9✔
197
    """Return the default value for a configuration option.
198

199
    The _get_param() function is a utility function used to get the value of a
200
    parameter for a module based on the default parameter settings and any
201
    arguments passed to the function.  The precedence order for parameters is
202
    the value passed to the function (as a keyword), the value from the
203
    `config.defaults` dictionary, and the default value `defval`.
204

205
    Parameters
206
    ----------
207
    module : str
208
        Name of the module whose parameters are being requested.
209
    param : str
210
        Name of the parameter value to be determined.
211
    argval : object or dict
212
        Value of the parameter as passed to the function.  This can either be
213
        an object or a dictionary (i.e. the keyword list from the function
214
        call).  Defaults to None.
215
    defval : object
216
        Default value of the parameter to use, if it is not located in the
217
        `config.defaults` dictionary.  If a dictionary is provided, then
218
        'module.param' is used to determine the default value.  Defaults to
219
        None.
220
    pop : bool, optional
221
        If True and if argval is a dict, then pop the remove the parameter
222
        entry from the argval dict after retrieving it.  This allows the use
223
        of a keyword argument list to be passed through to other functions
224
        internal to the function being called.
225
    last : bool, optional
226
        If True, check to make sure dictionary is empty after processing.
227

228
    """
229

230
    # Make sure that we were passed sensible arguments
231
    if not isinstance(module, str) or not isinstance(param, str):
9✔
232
        raise ValueError("module and param must be strings")
×
233

234
    # Construction the name of the key, for later use
235
    key = module + '.' + param
9✔
236

237
    # If we were passed a dict for the argval, get the param value from there
238
    if isinstance(argval, dict):
9✔
239
        val = argval.pop(param, None) if pop else argval.get(param, None)
9✔
240
        if last and argval:
9✔
241
            raise TypeError("unrecognized keywords: " + str(argval))
9✔
242
        argval = val
9✔
243

244
    # If we were passed a dict for the defval, get the param value from there
245
    if isinstance(defval, dict):
9✔
246
        defval = defval.get(key, None)
9✔
247

248
    # Return the parameter value to use (argval > defaults > defval)
249
    return argval if argval is not None else defaults.get(key, defval)
9✔
250

251

252
# Set defaults to match MATLAB
253
def use_matlab_defaults():
9✔
254
    """Use MATLAB compatible configuration settings.
255

256
    The following conventions are used:
257
        * Bode plots plot gain in dB, phase in degrees, frequency in
258
          rad/sec, with grids
259
        * Frequency plots use the label "Magnitude" for the system gain.
260

261
    Examples
262
    --------
263
    >>> ct.use_matlab_defaults()
264
    >>> # do some matlab style plotting
265

266
    """
267
    set_defaults('freqplot', dB=True, deg=True, Hz=False, grid=True)
9✔
268
    set_defaults('freqplot', magnitude_label="Magnitude")
9✔
269

270

271
# Set defaults to match FBS (Astrom and Murray)
272
def use_fbs_defaults():
9✔
273
    """Use Feedback Systems (FBS) compatible settings.
274

275
    The following conventions from `Feedback Systems <https://fbsbook.org>`_
276
    are used:
277

278
        * Bode plots plot gain in powers of ten, phase in degrees,
279
          frequency in rad/sec, no grid
280
        * Frequency plots use the label "Gain" for the system gain.
281
        * Nyquist plots use dashed lines for mirror image of Nyquist curve
282

283
    Examples
284
    --------
285
    >>> ct.use_fbs_defaults()
286
    >>> # do some FBS style plotting
287

288
    """
289
    set_defaults('freqplot', dB=False, deg=True, Hz=False, grid=False)
9✔
290
    set_defaults('freqplot', magnitude_label="Gain")
9✔
291
    set_defaults('nyquist', mirror_style='--')
9✔
292

293

294
def use_legacy_defaults(version):
9✔
295
    """ Sets the defaults to whatever they were in a given release.
296

297
    Parameters
298
    ----------
299
    version : string
300
        Version number of `python-control` to use for setting defaults.
301

302
    Examples
303
    --------
304
    >>> ct.use_legacy_defaults("0.9.0")
305
    (0, 9, 0)
306
    >>> # do some legacy style plotting
307

308
    """
309
    import re
9✔
310
    (major, minor, patch) = (None, None, None)  # default values
9✔
311

312
    # Early release tag format: REL-0.N
313
    match = re.match(r"^REL-0.([12])$", version)
9✔
314
    if match: (major, minor, patch) = (0, int(match.group(1)), 0)
9✔
315

316
    # Early release tag format: control-0.Np
317
    match = re.match(r"^control-0.([3-6])([a-d])$", version)
9✔
318
    if match: (major, minor, patch) = \
9✔
319
       (0, int(match.group(1)), ord(match.group(2)) - ord('a') + 1)
320

321
    # Early release tag format: v0.Np
322
    match = re.match(r"^[vV]?0\.([3-6])([a-d])$", version)
9✔
323
    if match: (major, minor, patch) = \
9✔
324
       (0, int(match.group(1)), ord(match.group(2)) - ord('a') + 1)
325

326
    # Abbreviated version format: vM.N or M.N
327
    match = re.match(r"^[vV]?([0-9]*)\.([0-9]*)$", version)
9✔
328
    if match: (major, minor, patch) = \
9✔
329
       (int(match.group(1)), int(match.group(2)), 0)
330

331
    # Standard version format: vM.N.P or M.N.P
332
    match = re.match(r"^[vV]?([0-9]*)\.([0-9]*)\.([0-9]*)$", version)
9✔
333
    if match: (major, minor, patch) = \
9✔
334
        (int(match.group(1)), int(match.group(2)), int(match.group(3)))
335

336
    # Make sure we found match
337
    if major is None or minor is None:
9✔
338
        raise ValueError("Version number not recognized. Try M.N.P format.")
9✔
339

340
    #
341
    # Go backwards through releases and reset defaults
342
    #
343
    reset_defaults()            # start from a clean slate
9✔
344

345
    # Version 0.10.2:
346
    if major == 0 and minor < 10 or (minor == 10 and patch < 2):
9✔
347
        from math import inf
9✔
348

349
        # Reset Nyquist defaults
350
        set_defaults('nyquist', arrows=2, max_curve_magnitude=20,
9✔
351
                     blend_fraction=0, indent_points=50)
352

353
    # Version 0.9.2:
354
    if major == 0 and minor < 9 or (minor == 9 and patch < 2):
9✔
355
        from math import inf
9✔
356

357
        # Reset Nyquist defaults
358
        set_defaults('nyquist', indent_radius=0.1, max_curve_magnitude=inf,
9✔
359
                     max_curve_offset=0, primary_style=['-', '-'],
360
                     mirror_style=['--', '--'], start_marker_size=0)
361

362
    # Version 0.9.0:
363
    if major == 0 and minor < 9:
9✔
364
        # switched to 'array' as default for state space objects
365
        warnings.warn("NumPy matrix class no longer supported")
9✔
366

367
        # switched to 0 (=continuous) as default timebase
368
        set_defaults('control', default_dt=None)
9✔
369

370
        # changed iosys naming conventions
371
        set_defaults('iosys', state_name_delim='.',
9✔
372
                     duplicate_system_name_prefix='copy of ',
373
                     duplicate_system_name_suffix='',
374
                     linearized_system_name_prefix='',
375
                     linearized_system_name_suffix='_linearized')
376

377
        # turned off _remove_useless_states
378
        set_defaults('statesp', remove_useless_states=True)
9✔
379

380
        # forced_response no longer returns x by default
381
        set_defaults('forced_response', return_x=True)
9✔
382

383
        # time responses are only squeezed if SISO
384
        set_defaults('control', squeeze_time_response=True)
9✔
385

386
        # switched mirror_style of nyquist from '-' to '--'
387
        set_defaults('nyquist', mirror_style='-')
9✔
388

389
    return (major, minor, patch)
9✔
390

391

392
def _process_legacy_keyword(kwargs, oldkey, newkey, newval, warn_oldkey=True):
9✔
393
    """Utility function for processing legacy keywords.
394

395
    .. deprecated:: 0.10.2
396
        Replace with `_process_param` or `_process_kwargs`.
397

398
    Use this function to handle a legacy keyword that has been renamed.
399
    This function pops the old keyword off of the kwargs dictionary and
400
    issues a warning.  If both the old and new keyword are present, a
401
    `ControlArgument` exception is raised.
402

403
    Parameters
404
    ----------
405
    kwargs : dict
406
        Dictionary of keyword arguments (from function call).
407
    oldkey : str
408
        Old (legacy) parameter name.
409
    newkey : str
410
        Current name of the parameter.
411
    newval : object
412
        Value of the current parameter (from the function signature).
413
    warn_oldkey : bool
414
        If set to False, suppress generation of a warning about using a
415
        legacy keyword.  This is useful if you have two versions of a
416
        keyword and you want to allow either to be used (see the `cost` and
417
        `trajectory_cost` keywords in `flatsys.point_to_point` for an
418
        example of this).
419

420
    Returns
421
    -------
422
    val : object
423
        Value of the (new) keyword.
424

425
    """
426
    # TODO: turn on this warning when ready to deprecate
427
    # warnings.warn(
428
    #     "replace `_process_legacy_keyword` with `_process_param` "
429
    #     "or `_process_kwargs`", PendingDeprecationWarning)
430
    if oldkey in kwargs:
9✔
431
        if warn_oldkey:
9✔
432
            warnings.warn(
9✔
433
                f"keyword '{oldkey}' is deprecated; use '{newkey}'",
434
                FutureWarning, stacklevel=3)
435
        if newval is not None:
9✔
436
            raise ControlArgument(
9✔
437
                f"duplicate keywords '{oldkey}' and '{newkey}'")
438
        else:
439
            return kwargs.pop(oldkey)
9✔
440
    else:
441
        return newval
9✔
442

443

444
def _process_param(name, defval, kwargs, alias_mapping, sigval=None):
9✔
445
    """Process named parameter, checking aliases and legacy usage.
446

447
    Helper function to process function arguments by mapping aliases to
448
    either their default keywords or to a named argument.  The alias
449
    mapping is a dictionary that returns a tuple consisting of valid
450
    aliases and legacy aliases::
451

452
       alias_mapping = {
453
            'argument_name_1': (['alias', ...], ['legacy', ...]),
454
            ...}
455

456
    If `param` is a named keyword in the function signature with default
457
    value `defval`, a typical calling sequence at the start of a function
458
    is::
459

460
        param = _process_param('param', defval, kwargs, function_aliases)
461

462
    If `param` is a variable keyword argument (in `kwargs`), `defval` can
463
    be passed as either None or the default value to use if `param` is not
464
    present in `kwargs`.
465

466
    Parameters
467
    ----------
468
    name : str
469
        Name of the parameter to be checked.
470
    defval : object or dict
471
        Default value for the parameter.
472
    kwargs : dict
473
        Dictionary of variable keyword arguments.
474
    alias_mapping : dict
475
        Dictionary providing aliases and legacy names.
476
    sigval : object, optional
477
        Default value specified in the function signature (default = None).
478
        If specified, an error will be generated if `defval` is different
479
        than `sigval` and an alias or legacy keyword is given.
480

481
    Returns
482
    -------
483
    newval : object
484
        New value of the named parameter.
485

486
    Raises
487
    ------
488
    TypeError
489
        If multiple keyword aliases are used for the same parameter.
490

491
    Warns
492
    -----
493
    PendingDeprecationWarning
494
        If legacy name is used to set the value for the variable.
495

496
    """
497
    # Check to see if the parameter is in the keyword list
498
    if name in kwargs:
9✔
499
        if defval != sigval:
9✔
500
            raise TypeError(f"multiple values for parameter {name}")
9✔
501
        newval = kwargs.pop(name)
9✔
502
    else:
503
        newval = defval
9✔
504

505
    # Get the list of aliases and legacy names
506
    aliases, legacy = alias_mapping[name]
9✔
507

508
    for kw in legacy:
9✔
509
        if kw in kwargs:
9✔
510
            warnings.warn(
×
511
                f"alias `{kw}` is legacy name; use `{name}` instead",
512
                PendingDeprecationWarning)
513
            kwval = kwargs.pop(kw)
×
514
            if newval != defval and kwval != newval:
×
515
                raise TypeError(
×
516
                    f"multiple values for parameter `{name}` (via {kw})")
517
            newval = kwval
×
518

519
    for kw in aliases:
9✔
520
        if kw in kwargs:
9✔
521
            kwval = kwargs.pop(kw)
×
522
            if newval != defval and kwval != newval:
×
523
                raise TypeError(
×
524
                    f"multiple values for parameter `{name}` (via {kw})")
525
            newval = kwval
×
526

527
    return newval
9✔
528

529

530
def _process_kwargs(kwargs, alias_mapping):
9✔
531
    """Process aliases and legacy keywords.
532

533
    Helper function to process function arguments by mapping aliases to
534
    their default keywords.  The alias mapping is a dictionary that returns
535
    a tuple consisting of valid aliases and legacy aliases::
536

537
       alias_mapping = {
538
            'argument_name_1': (['alias', ...], ['legacy', ...]),
539
            ...}
540

541
    If an alias is present in the dictionary of keywords, it will be used
542
    to set the value of the argument.  If a legacy keyword is used, a
543
    warning is issued.
544

545
    Parameters
546
    ----------
547
    kwargs : dict
548
        Dictionary of variable keyword arguments.
549
    alias_mapping : dict
550
        Dictionary providing aliases and legacy names.
551

552
    Raises
553
    ------
554
    TypeError
555
        If multiple keyword aliased are used for the same parameter.
556

557
    Warns
558
    -----
559
    PendingDeprecationWarning
560
        If legacy name is used to set the value for the variable.
561

562
    """
563
    for name in alias_mapping or []:
9✔
564
        aliases, legacy = alias_mapping[name]
9✔
565

566
        for kw in legacy:
9✔
567
            if kw in kwargs:
9✔
568
                warnings.warn(
9✔
569
                    f"alias `{kw}` is legacy name; use `{name}` instead",
570
                    PendingDeprecationWarning)
571
                if name in kwargs:
9✔
572
                    raise TypeError(
×
573
                        f"multiple values for parameter `{name}` (via {kw})")
574
                kwargs[name] = kwargs.pop(kw)
9✔
575

576
        for kw in aliases:
9✔
577
            if kw in kwargs:
9✔
578
                if name in kwargs:
9✔
579
                    raise TypeError(
×
580
                        f"multiple values for parameter `{name}` (via {kw})")
581
                kwargs[name] = kwargs.pop(kw)
9✔
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