-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpolynomial.py
More file actions
646 lines (542 loc) · 22.2 KB
/
polynomial.py
File metadata and controls
646 lines (542 loc) · 22.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
import json
import numpy as np
from ast import literal_eval
from typedmapping import TypedMapping, TypedMappingMeta
from formatting import pretty_json_string
class POPxfPolynomialMeta(TypedMappingMeta):
"""
Base metaclass for the `POPxfPolynomial` class.
Implements additional custom exception classes on top of TypedMappingMeta.
"""
def __init__(self, classname, baseclasses, attrs):
# attrs['key_order_error'] = type(
self.key_order_error = type(
classname+'KeyOrderError',
(attrs['base_error'],),
{}
)
self.length_error = type(
classname+'LengthError',
(attrs['base_error'],),
{}
)
self.shape_error = type(
classname+'ShapeError',
(attrs['base_error'],),
{}
)
super().__init__(classname, baseclasses, attrs)
class POPxfPolynomial(TypedMapping, metaclass=POPxfPolynomialMeta):
"""
Class to store polynomial data in POPxf JSON files.
This class represents polynomial expressions where coefficients are stored as
numpy arrays. Keys represent polynomial terms (monomials of parameters),
and values are the corresponding coefficient arrays. The class supports
polynomials of arbitrary degree with real and imaginary components.
Parameters
----------
data : dict
Dictionary containing polynomial data. Keys must be tuples representing
polynomial terms, and values must be float arrays or lists convertible
to numpy arrays.
degree : int, optional
Degree of the polynomial. Default is 2.
length : int, optional
Length of coefficient arrays. If not provided, inferred from the first
entry in `data`.
Attributes
----------
degree : int
Degree of the polynomial.
length : int
Length of the coefficient arrays.
shape : tuple
Shape of coefficient arrays (always 1D).
parameters : tuple
Tuple of parameter names that appear in the polynomial, sorted alphabetically.
key_types : tuple
Allowed types for keys (tuple only).
value_types : tuple
Allowed types for values (float and numpy.ndarray).
Raises
------
POPxfPolynomialInitError
If initialization fails due to invalid length, shape, or data format.
POPxfPolynomialKeyError
If keys are improperly formatted or violate ordering constraints.
POPxfPolynomialValueError
If values cannot be converted to proper numpy arrays.
POPxfPolynomialLengthError
If array lengths don't match the expected length.
POPxfPolynomialShapeError
If arrays have incorrect dimensionality.
Notes
-----
Keys must be tuples where:
- The first n elements are parameter names (strings)
- The last element (optional) is a real/imaginary specifier string of 'R' and 'I' characters
- Parameters must be ordered alphabetically
- Empty strings represent the constant term
Examples
--------
>>> data = {
... ('',''): [1.0, 2.0, 3.0],
... ('C1', ''): [0.1, 0.2, 0.3],
... ('C1', 'C2'): [0.01, 0.02, 0.03]
... }
>>> poly = POPxfPolynomial(data, degree=2, length=3)
>>> print(poly.parameters)
('C1', 'C2')
See Also
--------
POPxfPolynomialUncertainty : Subclass for handling polynomial uncertainties
TypedMapping : Base class providing typed dictionary functionality
"""
# allowed key and value types
key_types = (tuple,)
value_types = (float, np.ndarray)
def __init__(self, data, degree=2, length=None):
# polynomial degree
self.degree = degree
# specify length if given
if length is not None:
if isinstance(length, int):
self.length = length
else:
raise self.init_error(
f'Invalid length "{length}": must be an int.'
)
# otherwise infer shape from first datum
elif data:
_, first_val = next(iter(data.items()))
if isinstance(first_val, list):
try:
parsed_val = np.asarray(first_val)
except Exception as e:
raise self.init_error(
f'Could not convert "{first_val}" to numpy.ndarray when '
f'inferring length.'
) from e
self.shape = parsed_val.shape
elif isinstance(first_val, np.ndarray):
self.shape = first_val.shape
else:
raise self.init_error(
f'Could not convert infer length from "{first_val}".'
)
if len(self.shape)!=1:
# forbid ndarrays with more than one dimension
raise self.init_error(
f'Encoutered bad shape when inferring length from '
f'"{first_val}". Only one-dimensional arrays are allowed as '
'values.'
)
else:
self.length = self.shape[0]
else:
raise self.init_error(
'Length must be specified if data is empty.'
)
super().__init__(data)
# store the raw input data
self._raw_data = data
# tuple of parameters on which the polynomial depends
self.parameters = tuple(sorted(list(set([
x for y in
[list(i) if len(i)==self.degree else list(i[:-1]) for i in self.keys()]
for x in y if x
]))))
@staticmethod
def is_RI(element):
"""
Check if element is a real/imaginary specifier string.
A valid real/imaginary specifier string consists only of 'R' and 'I'
characters, where 'R' denotes real component and 'I' denotes imaginary
component.
Parameters
----------
element : str or None
String to check for real/imaginary specifier format.
Returns
-------
bool
True if element is a non-empty string containing only 'R' and 'I'
characters, False otherwise.
"""
return ( element and all([x in ('R', 'I') for x in element]) )
def _parse_key(self, key):
"""
Parse key, checking it is valid and return as a tuple.
`key` must be a tuple of strings. The first n elements represent
polynomial parameters. Optionally the last element can be a length-n
string where each element can only be 'R' or 'I', stating whether the
real or imaginary component of each coefficient is being referenced.
The constant term in the polynomial is specified by a tuple of empty
strings. Elements should be ordered alphabetically by convention.
If the real/imaginary specifier is not present, parameters are assumed
to be real.
If the key is a string, it is converted to a tuple via `ast.literal_eval()`.
Parameters
----------
key : tuple
Key to parse, checking validity as described above.
Returns
-------
tuple
Parsed key as a tuple of three strings.
Raises
------
self.key_error
If the key is not a valid tuple as described above.
self.key_order_error
If the parameters in the key are not ordered alphabetically.
Notes
-----
Most of the checks should automatically be checked by the JSON schema
validation step, so they could be skipped in principle. Only the
ordering check on the parameter names is not covered by the schema.
"""
tuplekey = literal_eval(key) if isinstance(key, str) else key
if not isinstance(tuplekey, tuple) or not tuplekey:
raise self.key_error(
f'Invalid key {key}: must be a non-empty tuple or string that '
'returns a non empty-tuple via literal_eval().'
) from None
# Handle optional real/imaginary specifier
if len(tuplekey) == self.degree:
params, RI_str = tuplekey, None
elif len(tuplekey) == self.degree + 1:
params, RI_str = tuplekey[:-1], tuplekey[-1]
else:
raise self.key_error(
f'Invalid key {key}: number of elements must be either equal to '
f'the polynomial degree ({self.degree}) or one more than the '
'polynomial degree (including real/imaginary specifier).'
)
# ensure formatting of key conforms to specification
if not all( isinstance(x, str) for x in params ):
raise self.key_error(
f'Invalid key {key}: all elements must be strings.'
)
elif len(params) != self.degree:
raise self.key_error(
f'Invalid key {key}: number of parameters ({len(params)}) '
f'must match polynomial degree ({self.degree}).'
)
elif RI_str is not None and len(params)!=len(RI_str):
raise self.key_error(
f'Invalid key {key}: Length of real/imaginary specifier string '
f'(currently {len(RI_str)}) must match the specified degree of '
f'the polynomial ({self.degree}).'
)
elif sorted(params) != list(params):
raise self.key_order_error(
f'Parameters {params} must be ordered alphabetically in '
f'{tuplekey}.'
)
else:
return super()._parse_key(tuplekey)
def _parse_value(self, value):
"""
Parse value, checking it is valid and return it, converting lists to
`numpy.ndarray`
`value` must be a `numpy.ndarray` or a list of floats that is
convertible to `numpy.ndarray`. Lists will be converted
to `numpy.ndarray`.
Parameters
----------
value : list or numpy.ndarray
Value to parse, checking validity as described above.
Returns
-------
numpy.ndarray
Parsed value.
Raises
------
self.value_error
If the value is not a valid `numpy.ndarray` or list as described above.
self.length_error
If the length of the array does not match `self.length`.
self.shape_error
If the array is not one-dimensional.
"""
if isinstance(value, np.ndarray):
if len(value.shape) != 1:
raise self.shape_error(
f'Invalid shape "{value}": array must be 1D.'
)
elif value.shape[0] != self.length:
raise self.length_error(
f'Invalid value "{value}": array must have length {self.length}.'
)
try:
newvalue = value.astype(float)
except Exception as e:
raise self.value_error(
f'Could not convert "{value}" to numpy.ndarray with type float.'
) from e
return super()._parse_value(newvalue)
# convert to np.ndarray if list
if isinstance(value, list):
try:
newvalue = np.asarray(value)
except Exception as e:
raise self.value_error(
f'Could not convert "{value}" to numpy.ndarray.'
) from e
return self._parse_value(newvalue)
else:
raise self.value_error(
f'Invalid value "{value}": must be a 1D numpy.ndarray or '
'list of floats convertible to numpy.ndarray.'
)
def to_str_dict(self, suppress_RI=False, use_raw=False):
"""
Convert the polynomial dictionary to a dictionary with keys as strings.
This method serializes the polynomial data structure into a plain Python
dictionary with string keys and list values, suitable for JSON serialization.
Parameters
----------
suppress_RI : bool, optional
If True, remove the real/imaginary specifier string from keys.
Default is False.
use_raw : bool, optional
If True, use the raw input data instead of the parsed internal
representation. Default is False.
Returns
-------
dict
Dictionary with string representations of keys and values converted
from numpy arrays to lists.
"""
result = dict()
source = self if not use_raw else self._raw_data
for key, val in source.items():
if suppress_RI and len(key)==self.degree+1:
key = key[:-1]
result[str(key)] = (
val.tolist() if isinstance(val, np.ndarray) else val
)
return result
def to_jstr(self, suppress_RI=False, use_raw=False):
"""
Convert polynomial to a formatted JSON string representation.
This method produces a pretty-printed JSON string of the polynomial data,
suitable for writing to POPxf JSON files.
Parameters
----------
suppress_RI : bool, optional
If True, remove the real/imaginary specifier string from keys.
Default is False.
use_raw : bool, optional
If True, use the raw input data instead of the parsed internal
representation. Default is False.
Returns
-------
str
Formatted JSON string representation of the polynomial dictionary.
See Also
--------
to_str_dict : Convert to plain dictionary
to_dict : Convert to dictionary via JSON round-trip
"""
result = json.dumps(
self.to_str_dict(suppress_RI=suppress_RI, use_raw=use_raw),
default = lambda x: x.to_str_dict(suppress_RI=suppress_RI),
indent=2
)
return pretty_json_string(result)
def to_dict(self):
"""
Convert polynomial to a plain Python dictionary.
This method performs a JSON round-trip conversion (to JSON string and back)
to ensure all values are standard Python types.
Returns
-------
dict
Plain Python dictionary representation of the polynomial.
See Also
--------
to_jstr : Convert to JSON string
to_str_dict : Convert to string-keyed dictionary
Notes
-----
This method uses JSON serialization internally, so all numpy arrays
are converted to lists.
"""
return json.loads(self.to_jstr())
# def evaluate(self, *args, **kwargs):
# """
# Evaluate the polynomial at given parameter values.
# Parameters
# ----------
# *args : float
# Parameter values in the order specified by `self.parameters`.
# **kwargs : float
# Parameter values specified by name.
# Returns
# -------
# numpy.ndarray
# Evaluated polynomial as a numpy array.
# """
# if args and kwargs:
# raise ValueError(
# 'Cannot specify both positional and keyword arguments when '
# 'evaluating polynomial.'
# )
# if args:
# if len(args) != len(self.parameters):
# raise ValueError(
# f'Invalid number of positional arguments ({len(args)}): '
# f'must match number of parameters ({len(self.parameters)}).'
# )
# param_values = dict(zip(self.parameters, args))
# else:
# param_values = kwargs
# result = np.zeros(self.length)
# # unfinished
# for key, coeff in self.items():
# # assume real coefficients if no RI specifier given
# if not self.is_RI(key[-1]):
# key = tuple(*key,'R'*len(key))
# term_value = coeff
# for i, param in enumerate(key[:-1]):
# ri_flag = key[-1][i] if self.is_RI(key[-1]) else 'R'
# param_value = param_values.get(param, 0.0)
# if ri_flag == 'I':
# param_value = 0.0 # Imaginary part is zero for real inputs
# term_value *= param_value
# result += term_value
# return result
class POPxfPolynomialUncertainty(POPxfPolynomial):
"""
Class to store polynomial uncertainty data in POPxf JSON files.
This class extends `POPxfPolynomial` to handle uncertainty specifications that
may be parameter-independent or parameter-dependent. When uncertainty is
parameter-independent (constant across all parameter values), it can be
specified as a simple array rather than a full polynomial dictionary.
Parameters
----------
data : dict, list, or numpy.ndarray
Uncertainty data. Can be either:
- A dictionary with the same structure as `POPxfPolynomial` for
parameter-dependent uncertainties
- A 1D list or numpy array for parameter-independent (constant)
uncertainties, which will be internally converted to a polynomial
with a single constant term
degree : int, optional
Degree of the polynomial. Default is 2.
length : int, optional
Length of coefficient arrays. If not provided, inferred from the first
entry in `data`.
precision : float, optional
Numerical precision for operations (currently unused).
Attributes
----------
degree : int
Degree of the polynomial (inherited from parent).
length : int
Length of the coefficient arrays (inherited from parent).
parameters : tuple
Tuple of parameter names that appear in the polynomial (inherited from parent).
Raises
------
POPxfPolynomialInitError
If data is a list/array but not 1D, or if other initialization constraints
from the parent class are violated.
Notes
-----
When `data` is provided as a list or numpy array, it represents a parameter-
independent uncertainty. This is internally converted to a polynomial dictionary
with a single entry: the constant term represented by a tuple of empty strings.
The `_raw_data` attribute stores the original input data format, which is used
by serialization methods to preserve the simplified representation for
parameter-independent uncertainties.
See Also
--------
POPxfPolynomial : Parent class for polynomial data storage
"""
def __init__(self, data, degree=2, length=None):
# handle special case of parameter independent uncertainty
if isinstance(data, (list, np.ndarray)):
if len(np.asarray(data).shape)!=1:
raise self.init_error(
f'Invalid data "{data}": must be a 1D list or numpy.ndarray '
'for parameter independent uncertainty.'
)
newdata = { ('',)*degree : data }
else:
newdata = data
super().__init__(newdata, degree=degree, length=length)
self._raw_data = data
def evaluate(self):
return NotImplementedError(
'Evaluation of polynomial uncertainties is not implemented.'
)
def to_str_dict(self, suppress_RI=False, use_raw=True):
"""
Convert the uncertainty polynomial to a dictionary with keys as strings.
This method overrides the parent class method to handle the special case
of parameter-independent uncertainties, which can be serialized as a
simple list string rather than a full dictionary.
Parameters
----------
suppress_RI : bool, optional
If True, remove the real/imaginary specifier string from keys.
Default is False.
use_raw : bool, optional
If True, use the raw input data instead of the parsed internal
representation. Default is True (unlike parent class default).
Returns
-------
dict or str
If the raw data is a list or numpy array (parameter-independent
uncertainty), returns a string representation of the array.
Otherwise, returns a dictionary with string keys and list values
as in the parent class.
Notes
-----
The default value of `use_raw=True` differs from the parent class to
preserve the simplified representation of parameter-independent
uncertainties in serialized output.
See Also
--------
POPxfPolynomial.to_str_dict : Parent class method
"""
source = self if not use_raw else self._raw_data
# handle special case of parameter independent uncertainty
if isinstance(source, list):
return str(source)
elif isinstance(source, np.ndarray):
return str(source.tolist())
else:
return super().to_str_dict(suppress_RI=suppress_RI, use_raw=use_raw)
def to_jstr(self, suppress_RI=False, use_raw=True):
"""
Convert uncertainty polynomial to a formatted JSON string representation.
This method calls the parent class method with `use_raw=True` by default
to preserve the simplified representation of parameter-independent
uncertainties.
Parameters
----------
suppress_RI : bool, optional
If True, remove the real/imaginary specifier string from keys.
Default is False.
use_raw : bool, optional
If True, use the raw input data instead of the parsed internal
representation. Default is True (unlike parent class default).
Returns
-------
str
Formatted JSON string representation of the uncertainty data.
Notes
-----
The default value of `use_raw=True` differs from the parent class to
preserve the simplified representation of parameter-independent
uncertainties in JSON output.
See Also
--------
POPxfPolynomial.to_jstr : Parent class method
to_str_dict : Convert to string-keyed dictionary
"""
return super().to_jstr(suppress_RI=suppress_RI, use_raw=use_raw)