Skip to content

Commit 4ab8e2d

Browse files
authored
Merge pull request #117 from bnavigator/slycot-error
Add Slycot exception and warning classes
2 parents 63b1674 + fd74855 commit 4ab8e2d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3001
-3122
lines changed

slycot/analysis.py

Lines changed: 407 additions & 472 deletions
Large diffs are not rendered by default.

slycot/exceptions.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
"""
2+
exceptions.py
3+
4+
Copyright 2020 Slycot team
5+
6+
This program is free software; you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License version 2 as
8+
published by the Free Software Foundation.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18+
MA 02110-1301, USA.
19+
"""
20+
21+
import re
22+
23+
from warnings import warn
24+
25+
26+
class SlycotError(RuntimeError):
27+
"""Slycot exception base class"""
28+
29+
def __init__(self, message, info):
30+
super(SlycotError, self).__init__(message)
31+
self.info = info
32+
33+
34+
class SlycotParameterError(SlycotError, ValueError):
35+
"""A Slycot input parameter had an illegal value.
36+
37+
In case of a wrong input value, the SLICOT routines return a negative
38+
info parameter indicating which parameter was illegal.
39+
"""
40+
41+
pass
42+
43+
44+
class SlycotArithmeticError(SlycotError, ArithmeticError):
45+
"""A Slycot computation failed"""
46+
47+
pass
48+
49+
50+
class SlycotWarning(UserWarning):
51+
"""Slycot Warning"""
52+
53+
def __init__(self, message, iwarn, info):
54+
super(SlycotWarning, self).__init__(message)
55+
self.info = info
56+
self.iwarn = iwarn
57+
58+
59+
class SlycotResultWarning(SlycotWarning):
60+
"""Slycot computation result warning
61+
62+
A Slycot routine returned a nonzero info parameter that warns about the
63+
returned results, but the results might still be usable.
64+
"""
65+
66+
pass
67+
68+
69+
def _parse_docsection(section_name, docstring, checkvars):
70+
slycot_error = None
71+
message = None
72+
docline = iter(docstring.splitlines())
73+
try:
74+
75+
info_eval = False
76+
while section_name not in next(docline):
77+
continue
78+
section_indent = next(docline).index("-")
79+
80+
for l in docline:
81+
# ignore blank lines
82+
if not l.strip():
83+
continue
84+
85+
# reached next section without match
86+
if l[section_indent] == "-":
87+
break
88+
89+
# Exception Type
90+
ematch = re.match(
91+
r'(\s*)(Slycot.*(Error|Warning))', l)
92+
if ematch:
93+
error_indent = len(ematch.group(1))
94+
slycot_error = ematch.group(2)
95+
96+
# new infospec
97+
if slycot_error:
98+
imatch = re.match(
99+
r'(\s{' + str(error_indent + 1) + r',}):(.+):\s*(.*)', l)
100+
if imatch:
101+
infospec_indent = len(imatch.group(1))
102+
infospec = imatch.group(2)
103+
# Don't handle the standard case unless we have i
104+
if infospec == "info = -i":
105+
if 'i' not in checkvars.keys():
106+
continue
107+
infospec_ = infospec.replace(" = ", " == ")
108+
try:
109+
info_eval = eval(infospec_, checkvars)
110+
except NameError:
111+
raise RuntimeError("Unknown variable in infospec: "
112+
+ infospec)
113+
except SyntaxError:
114+
raise RuntimeError("Invalid infospec: " + infospec)
115+
if info_eval:
116+
message = imatch.group(3).strip() + '\n'
117+
mmatch = re.match(
118+
r'(\s{' + str(infospec_indent+1) + r',})(.*)',
119+
next(docline))
120+
if not mmatch:
121+
break # docstring
122+
body_indent = len(mmatch.group(1))
123+
message += mmatch.group(2) + '\n'
124+
for l in docline:
125+
if l and not l[:body_indent].isspace():
126+
break # message body
127+
message += l[body_indent:] + '\n'
128+
break # docstring
129+
except StopIteration:
130+
pass
131+
return (slycot_error, message)
132+
133+
134+
def raise_if_slycot_error(info, arg_list=None, docstring=None, checkvars=None):
135+
"""Raise exceptions or warnings if slycot info returned is non-zero.
136+
137+
Parameters
138+
----------
139+
info: int or list of int
140+
The parameter INFO or [IWARN, INFO] returned by the SLICOT subroutine
141+
arg_list: list of str, optional
142+
A list of arguments (possibly hidden by the wrapper) of the SLICOT
143+
subroutine
144+
docstring: str, optional
145+
The docstring of the Slycot function
146+
checkvars: dict, optional
147+
dict of variables for evaluation of <infospec> and formatting the
148+
exception message
149+
150+
Notes
151+
-----
152+
If the numpydoc compliant docstring has a "Raises" section with one or
153+
multiple definition terms ``SlycotError`` or a subclass of it,
154+
the matching exception text is used.
155+
156+
To raise warnings, define a "Warns" section using a ``SlycotWarning``
157+
definition or a subclass of it.
158+
159+
The definition body must contain a reST compliant field list with
160+
':<infospec>:' as field name, where <infospec> is a python parseable
161+
expression using the arguments `iwarn`, `info` and any additional variables
162+
provided in `checkvars` (usually obtained by calling `locals()`.
163+
A single " = " is treated as " == ".
164+
165+
The body of the field list contains the exception or warning message and
166+
can contain replacement fields in format string syntax using the variables
167+
in `checkvars`.
168+
169+
For negative info, the argument as indicated in arg_list was erroneous and
170+
a generic SlycotParameterError is raised if matching infospec was defined.
171+
172+
Example
173+
-------
174+
>>> def fun(info):
175+
... '''Example function
176+
...
177+
... Raises
178+
... ------
179+
... SlycotArithmeticError
180+
... :info = 1: INFO is 1
181+
... :info > 1 and info < n:
182+
... INFO is {info}, which is between 1 and {n}
183+
... :n <= info < m:
184+
... {info} is in [{n}, {m:10.2g})!
185+
...
186+
... Warns
187+
... -----
188+
... SlycotResultWarning
189+
... :info >= 120: {info} is too large
190+
... SlycotResultWarning
191+
... :iwarn == 1: IWARN is 1
192+
... '''
193+
... n, m = 4, 120.
194+
... raise_if_slycot_error(info,
195+
... arg_list=["a", "b", "c"],
196+
... docstring=(fun.__doc__ if type(info) is list
197+
... else fun.__doc__[:-60]),
198+
... checkvars=locals())
199+
...
200+
>>> fun(0)
201+
>>> fun(-1)
202+
SlycotParameterError:
203+
The following argument had an illegal value: a
204+
>>> fun(1)
205+
SlycotArithmeticError:
206+
INFO is 1
207+
>>> fun(2)
208+
SlycotArithmeticError:
209+
INFO is 2, which is between 1 and 4
210+
>>> fun(4)
211+
SlycotArithmeticError:
212+
4 is in [4, 1.2e+02)!
213+
>>> fun(120)
214+
SlycotResultWarning:
215+
120 is too large
216+
>>> fun([1,0])
217+
SlycotResultWarning:
218+
IWARN is 1
219+
"""
220+
try:
221+
iwarn, info = info
222+
except TypeError:
223+
iwarn = None
224+
if not checkvars:
225+
checkvars = {}
226+
if docstring and (iwarn or info):
227+
# possibly override info with mandatory argument
228+
checkvars['info'] = info
229+
# do not possibly override iwarn if not provided
230+
if iwarn is not None:
231+
checkvars['iwarn'] = iwarn
232+
233+
exception, message = _parse_docsection("Raises", docstring, checkvars)
234+
if exception and message:
235+
fmessage = '\n' + message.format(**checkvars).strip()
236+
raise globals()[exception](fmessage, info)
237+
238+
warning, message = _parse_docsection("Warns", docstring, checkvars)
239+
if warning and message:
240+
fmessage = '\n' + message.format(**checkvars).strip()
241+
warn(globals()[warning](fmessage, iwarn, info))
242+
return
243+
244+
if info < 0 and arg_list:
245+
message = ("The following argument had an illegal value: {}"
246+
"".format(arg_list[-info-1]))
247+
raise SlycotParameterError(message, info)
248+
249+
# catch all
250+
if info > 0:
251+
raise SlycotError("Caught unhandled nonzero INFO value {}"
252+
"".format(info),
253+
info)
254+
if iwarn is None and 'iwarn' in checkvars:
255+
iwarn = checkvars['iwarn']
256+
if iwarn:
257+
warn(SlycotWarning("Caught unhandled nonzero IWARN value {}"
258+
"".format(iwarn),
259+
iwarn, info))

0 commit comments

Comments
 (0)