aesoptparam 0.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aesoptparam might be problematic. Click here for more details.

@@ -0,0 +1,1060 @@
1
+ """
2
+ From: https://github.com/OpenMDAO/OpenMDAO/blob/master/openmdao/utils/units.py
3
+ Classes and functions to support unit conversion.
4
+
5
+ The module provides a basic set of predefined physical quantities
6
+ in its built-in library; however, it also supports generation of
7
+ personal libararies which can be saved and reused.
8
+ This module is based on the PhysicalQuantities module
9
+ in Scientific Python, by Konrad Hinsen. Modifications by
10
+ Justin Gray.
11
+ """
12
+
13
+ import os.path
14
+ import re
15
+ import sys
16
+ from collections import OrderedDict
17
+ from configparser import RawConfigParser as ConfigParser
18
+ from math import floor, pi
19
+
20
+ import numpy as np
21
+
22
+ ####################################
23
+ # Class Definitions
24
+ ####################################
25
+
26
+
27
+ class NumberDict(OrderedDict):
28
+ """
29
+ Dictionary storing numerical values.
30
+
31
+ An instance of this class acts like an array of numbers with
32
+ generalized (non-integer) indices. A value of zero is assumed
33
+ for undefined entries. NumberDict instances support addition
34
+ and subtraction with other NumberDict instances, and multiplication
35
+ and division by scalars.
36
+ """
37
+
38
+ def __getitem__(self, item):
39
+ """
40
+ Get the item, or 0.
41
+
42
+ Parameters
43
+ ----------
44
+ item : key
45
+ key to get the item
46
+
47
+ Returns
48
+ -------
49
+ int
50
+ value of the given key
51
+ """
52
+ try:
53
+ return dict.__getitem__(self, item)
54
+ except KeyError:
55
+ return 0
56
+
57
+ def __add__(self, other):
58
+ """
59
+ Add another NumberDict to myself.
60
+
61
+ Parameters
62
+ ----------
63
+ other : NumberDict
64
+ the other NumberDict Instance
65
+
66
+ Returns
67
+ -------
68
+ NumberDict
69
+ new NumberDict with self+other values
70
+ """
71
+ sum_dict = NumberDict()
72
+ for k, v in self.items():
73
+ sum_dict[k] = v
74
+ for k, v in other.items():
75
+ sum_dict[k] = sum_dict[k] + v
76
+ return sum_dict
77
+
78
+ def __sub__(self, other):
79
+ """
80
+ Add another NumberDict from myself.
81
+
82
+ Parameters
83
+ ----------
84
+ other : NumberDict
85
+ the other NumberDict Instance
86
+
87
+ Returns
88
+ -------
89
+ NumberDict
90
+ new NumberDict instance, with self-other values
91
+ """
92
+ sum_dict = NumberDict()
93
+ for k, v in self.items():
94
+ sum_dict[k] = v
95
+ for k, v in other.items():
96
+ sum_dict[k] = sum_dict[k] - v
97
+ return sum_dict
98
+
99
+ def __rsub__(self, other):
100
+ """
101
+ Add subtract myself from another NumberDict.
102
+
103
+ Parameters
104
+ ----------
105
+ other : NumberDict
106
+ the other NumberDict Instance
107
+
108
+ Returns
109
+ -------
110
+ NumberDict
111
+ new NumberDict instance, with other-self values
112
+ """
113
+ sum_dict = NumberDict()
114
+ for k, v in other.items():
115
+ sum_dict[k] = v
116
+ for k, v in self.items():
117
+ sum_dict[k] = sum_dict[k] - v
118
+ return sum_dict
119
+
120
+ def __mul__(self, other):
121
+ """
122
+ Multiply myself by another NumberDict.
123
+
124
+ Parameters
125
+ ----------
126
+ other : NumberDict
127
+ the other NumberDict Instance
128
+
129
+ Returns
130
+ -------
131
+ NumberDict
132
+ new NumberDict instance, with other*self values
133
+ """
134
+ new = NumberDict()
135
+ for key, value in self.items():
136
+ new[key] = other * value
137
+ return new
138
+
139
+ __rmul__ = __mul__
140
+
141
+ def __div__(self, other):
142
+ """
143
+ Divide myself by another NumberDict.
144
+
145
+ Parameters
146
+ ----------
147
+ other : int
148
+ value to divide by
149
+
150
+ Returns
151
+ -------
152
+ NumberDict
153
+ new NumberDict instance, with self/other values
154
+ """
155
+ new = NumberDict()
156
+ for key, value in self.items():
157
+ new[key] = value / other
158
+ return new
159
+
160
+ __truediv__ = __div__ # for python 3
161
+
162
+ def __repr__(self):
163
+ """
164
+ Return a string deceleration of myself.
165
+
166
+ Parameters
167
+ ----------
168
+ other : NumberDict
169
+ the other NumberDict Instance
170
+
171
+ Returns
172
+ -------
173
+ str
174
+ str representation for the creation of this NumberDict
175
+ """
176
+ return repr(dict(self))
177
+
178
+
179
+ class PhysicalUnit(object):
180
+ """
181
+ Physical unit.
182
+
183
+ A physical unit is defined by a name (possibly composite), a scaling
184
+ factor, and the exponentials of each of the SI base units that enter into
185
+ it. Units can be multiplied, divided, and raised to integer powers.
186
+
187
+ Parameters
188
+ ----------
189
+ names : dict or str
190
+ A dictionary mapping each name component to its
191
+ associated integer power (e.g., C{{'m': 1, 's': -1}})
192
+ for M{m/s}). As a shorthand, a string may be passed
193
+ which is assigned an implicit power 1.
194
+ factor : float
195
+ A scaling factor.
196
+ powers : list of int
197
+ The integer powers for each of the nine base units.
198
+ offset : float
199
+ An additive offset to the base unit (used only for temperatures).
200
+
201
+ Attributes
202
+ ----------
203
+ _names : dict or str
204
+ A dictionary mapping each name component to its
205
+ associated integer power (e.g., C{{'m': 1, 's': -1}})
206
+ for M{m/s}). As a shorthand, a string may be passed
207
+ which is assigned an implicit power 1.
208
+ _factor : float
209
+ A scaling factor.
210
+ _powers : list of int
211
+ The integer powers for each of the nine base units.
212
+ _offset : float
213
+ An additive offset to the base unit (used only for temperatures)
214
+ """
215
+
216
+ def __init__(self, names, factor, powers, offset=0):
217
+ """
218
+ Initialize all attributes.
219
+ """
220
+ if isinstance(names, str):
221
+ self._names = NumberDict(((names, 1),))
222
+ else:
223
+ self._names = names
224
+
225
+ self._factor = float(factor)
226
+ self._offset = float(offset)
227
+ self._powers = powers
228
+
229
+ def __repr__(self):
230
+ """
231
+ Get the string representation of this unit.
232
+
233
+ Returns
234
+ -------
235
+ str
236
+ str representation of how to instantiate this PhysicalUnit
237
+ """
238
+ return "PhysicalUnit(%s,%s,%s,%s)" % (
239
+ self._names,
240
+ self._factor,
241
+ self._powers,
242
+ self._offset,
243
+ )
244
+
245
+ def __str__(self):
246
+ """
247
+ Convert myself to string.
248
+
249
+ Returns
250
+ -------
251
+ str
252
+ str representation of a PhysicalUnit
253
+ """
254
+ return "<PhysicalUnit " + self.name() + ">"
255
+
256
+ def __lt__(self, other):
257
+ """
258
+ Compare myself to other.
259
+
260
+ Parameters
261
+ ----------
262
+ other : PhysicalUnit
263
+ The other physical unit to be compared to
264
+
265
+ Returns
266
+ -------
267
+ bool
268
+ self._factor < other._factor
269
+ """
270
+ if self._powers != other._powers or self._offset != other._offset:
271
+ raise TypeError(
272
+ f"Units '{self.name()}' and '{other.name()}' are incompatible."
273
+ )
274
+
275
+ return self._factor < other._factor
276
+
277
+ def __gt__(self, other):
278
+ """
279
+ Compare myself to other.
280
+
281
+ Parameters
282
+ ----------
283
+ other : PhysicalUnit
284
+ The other physical unit to be compared to
285
+
286
+ Returns
287
+ -------
288
+ bool
289
+ self._factor > other._factor
290
+ """
291
+ if self._powers != other._powers:
292
+ raise TypeError(
293
+ f"Units '{self.name()}' and '{other.name()}' are incompatible."
294
+ )
295
+ return self._factor > other._factor
296
+
297
+ def __eq__(self, other):
298
+ """
299
+ Test for equality.
300
+
301
+ Parameters
302
+ ----------
303
+ other : PhysicalUnit
304
+ The other physical unit to be compared to
305
+
306
+ Returns
307
+ -------
308
+ bool
309
+ true if _factor, _offset, and _powers all match
310
+ """
311
+ return (
312
+ self._factor == other._factor
313
+ and self._offset == other._offset
314
+ and self.is_compatible(other)
315
+ )
316
+
317
+ def __mul__(self, other):
318
+ """
319
+ Multiply myself by other.
320
+
321
+ Parameters
322
+ ----------
323
+ other : PhysicalUnit
324
+ The other physical unit to be compared to
325
+
326
+ Returns
327
+ -------
328
+ PhysicalUnit
329
+ new PhysicalUnit instance representing the product of two units
330
+ """
331
+ if self._offset != 0 or (
332
+ isinstance(other, PhysicalUnit) and other._offset != 0
333
+ ):
334
+ raise TypeError(
335
+ f"Can't multiply units: either '{self.name()}' or '{other.name()}' "
336
+ "has a non-zero offset."
337
+ )
338
+ if isinstance(other, PhysicalUnit):
339
+ return PhysicalUnit(
340
+ self._names + other._names,
341
+ self._factor * other._factor,
342
+ [a + b for a, b in zip(self._powers, other._powers)],
343
+ )
344
+ else:
345
+ return PhysicalUnit(
346
+ self._names + {str(other): 1},
347
+ self._factor * other,
348
+ self._powers,
349
+ self._offset * other,
350
+ )
351
+
352
+ __rmul__ = __mul__
353
+
354
+ def __div__(self, other):
355
+ """
356
+ Divide myself by other.
357
+
358
+ Parameters
359
+ ----------
360
+ other : PhysicalUnit
361
+ The other physical unit to be operated on
362
+
363
+ Returns
364
+ -------
365
+ PhysicalUnit
366
+ new PhysicalUnit instance representing the self/other
367
+ """
368
+ if self._offset != 0 or (
369
+ isinstance(other, PhysicalUnit) and other._offset != 0
370
+ ):
371
+ raise TypeError(
372
+ f"Can't divide units: either '{self.name()}' or '{other.name()}' "
373
+ "has a non-zero offset."
374
+ )
375
+ if isinstance(other, PhysicalUnit):
376
+ return PhysicalUnit(
377
+ self._names - other._names,
378
+ self._factor / other._factor,
379
+ [a - b for (a, b) in zip(self._powers, other._powers)],
380
+ )
381
+ else:
382
+ return PhysicalUnit(
383
+ self._names + {str(other): -1},
384
+ self._factor / float(other),
385
+ self._powers,
386
+ )
387
+
388
+ __truediv__ = __div__ # for python 3
389
+
390
+ def __rdiv__(self, other):
391
+ """
392
+ Divide other by myself.
393
+
394
+ Parameters
395
+ ----------
396
+ other : PhysicalUnit
397
+ The other physical unit to be operated on
398
+
399
+ Returns
400
+ -------
401
+ PhysicalUnit
402
+ new PhysicalUnit instance representing the other/self
403
+ """
404
+ return PhysicalUnit(
405
+ {str(other): 1} - self._names,
406
+ float(other) / self._factor,
407
+ [-x for x in self._powers],
408
+ )
409
+
410
+ __rtruediv__ = __rdiv__
411
+
412
+ def __pow__(self, power):
413
+ """
414
+ Raise myself to a power.
415
+
416
+ Parameters
417
+ ----------
418
+ power : float or int
419
+ power to raise self by
420
+
421
+ Returns
422
+ -------
423
+ PhysicalUnit
424
+ new PhysicalUnit of self^power
425
+ """
426
+ if self._offset != 0:
427
+ raise TypeError(
428
+ f"Can't exponentiate unit '{self.name()}' because it "
429
+ "has a non-zero offset."
430
+ )
431
+ if isinstance(power, int):
432
+ return PhysicalUnit(
433
+ power * self._names,
434
+ pow(self._factor, power),
435
+ [x * power for x in self._powers],
436
+ )
437
+ if isinstance(power, float):
438
+ inv_exp = 1.0 / power
439
+ rounded = int(floor(inv_exp + 0.5))
440
+ if abs(inv_exp - rounded) < 1.0e-10:
441
+ if all([x % rounded == 0 for x in self._powers]):
442
+ f = self._factor**power
443
+ p = [x / rounded for x in self._powers]
444
+ if all([x % rounded == 0 for x in self._names.values()]):
445
+ names = self._names / rounded
446
+ else:
447
+ names = NumberDict()
448
+ if f != 1.0:
449
+ names[str(f)] = 1
450
+ for x, name in zip(p, _UNIT_LIB.base_names):
451
+ names[name] = x
452
+ return PhysicalUnit(names, f, p)
453
+
454
+ raise TypeError(
455
+ f"Can't exponentiate unit '{self.name()}': "
456
+ "only integer and inverse integer exponents are allowed."
457
+ )
458
+
459
+ def in_base_units(self):
460
+ """
461
+ Return the base unit equivalent of this unit.
462
+
463
+ Returns
464
+ -------
465
+ PhysicalUnit
466
+ The equivalent base unit.
467
+ """
468
+ num = ""
469
+ denom = ""
470
+ for unit, power in zip(_UNIT_LIB.base_names, self._powers):
471
+ if power < 0:
472
+ denom = denom + "/" + unit
473
+ if power < -1:
474
+ denom = denom + "**" + str(-power)
475
+ elif power > 0:
476
+ num = num + "*" + unit
477
+ if power > 1:
478
+ num = num + "**" + str(power)
479
+
480
+ if len(num) == 0:
481
+ num = "1"
482
+ else:
483
+ num = num[1:]
484
+
485
+ return _find_unit(num + denom)
486
+
487
+ def conversion_tuple_to(self, other):
488
+ """
489
+ Compute the tuple of (factor, offset) for conversion.
490
+
491
+ Parameters
492
+ ----------
493
+ other : PhysicalUnit
494
+ Another unit.
495
+
496
+ Returns
497
+ -------
498
+ Tuple with two floats
499
+ The conversion factor and offset from this unit to another unit.
500
+ """
501
+ if not self.is_compatible(other):
502
+ raise TypeError(
503
+ f"Units '{self.name()}' and '{other.name()}' are incompatible."
504
+ )
505
+
506
+ # let (s1,d1) be the conversion tuple from 'self' to base units
507
+ # (ie. (x+d1)*s1 converts a value x from 'self' to base units,
508
+ # and (x/s1)-d1 converts x from base to 'self' units)
509
+ # and (s2,d2) be the conversion tuple from 'other' to base units
510
+ # then we want to compute the conversion tuple (S,D) from
511
+ # 'self' to 'other' such that (x+D)*S converts x from 'self'
512
+ # units to 'other' units
513
+ # the formula to convert x from 'self' to 'other' units via the
514
+ # base units is (by definition of the conversion tuples):
515
+ # ( ((x+d1)*s1) / s2 ) - d2
516
+ # = ( (x+d1) * s1/s2) - d2
517
+ # = ( (x+d1) * s1/s2 ) - (d2*s2/s1) * s1/s2
518
+ # = ( (x+d1) - (d1*s2/s1) ) * s1/s2
519
+ # = (x + d1 - d2*s2/s1) * s1/s2
520
+ # thus, D = d1 - d2*s2/s1 and S = s1/s2
521
+
522
+ factor = self._factor / other._factor
523
+ offset = self._offset - (other._offset * other._factor / self._factor)
524
+ return (factor, offset)
525
+
526
+ def is_compatible(self, other):
527
+ """
528
+ Check for compatibility with another unit.
529
+
530
+ Parameters
531
+ ----------
532
+ other : PhysicalUnit
533
+ Another unit.
534
+
535
+ Returns
536
+ -------
537
+ bool
538
+ Indicates if two units are compatible.
539
+ """
540
+ return self._powers == other._powers
541
+
542
+ def is_dimensionless(self):
543
+ """
544
+ Dimensionless PQ.
545
+
546
+ Returns
547
+ -------
548
+ bool
549
+ Indicates if this is dimensionless.
550
+ """
551
+ return not any(self._powers)
552
+
553
+ def is_angle(self):
554
+ """
555
+ Check if this PQ is an Angle.
556
+
557
+ Returns
558
+ -------
559
+ bool
560
+ Indicates if this an angle type.
561
+ """
562
+ return (
563
+ self._powers[_UNIT_LIB.base_types["angle"]] == 1 and sum(self._powers) == 1
564
+ )
565
+
566
+ def set_name(self, name):
567
+ """
568
+ Set the name.
569
+
570
+ Parameters
571
+ ----------
572
+ name : str
573
+ The name.
574
+ """
575
+ self._names = NumberDict()
576
+ self._names[name] = 1
577
+
578
+ def name(self):
579
+ """
580
+ Compute the name of this unit.
581
+
582
+ Returns
583
+ -------
584
+ str
585
+ String representation of the unit.
586
+ """
587
+ num = ""
588
+ denom = ""
589
+ for unit, power in self._names.items():
590
+ if power < 0:
591
+ denom = denom + "/" + unit
592
+ if power < -1:
593
+ denom = denom + "**" + str(-power)
594
+ elif power > 0:
595
+ num = num + "*" + unit
596
+ if power > 1:
597
+ num = num + "**" + str(power)
598
+ if len(num) == 0:
599
+ num = "1"
600
+ else:
601
+ num = num[1:]
602
+ return num + denom
603
+
604
+
605
+ ####################################
606
+ # Module Functions
607
+ ####################################
608
+
609
+
610
+ def _new_unit(name, factor, powers):
611
+ """
612
+ Create new Unit.
613
+
614
+ Parameters
615
+ ----------
616
+ name : str
617
+ The name of the new unit
618
+ factor : float
619
+ conversion factor to base units
620
+ powers : [int, ...]
621
+ power of base units
622
+
623
+ """
624
+ _UNIT_LIB.unit_table[name] = PhysicalUnit(name, factor, powers)
625
+
626
+
627
+ def add_offset_unit(name, baseunit, factor, offset, comment=""):
628
+ """
629
+ Adding Offset Unit.
630
+
631
+ Parameters
632
+ ----------
633
+ name : str
634
+ The name of the unit.
635
+ baseunit : str or instance of PhysicalUnit
636
+ The unit upon which this offset unit is based.
637
+ factor : str
638
+ The scaling factor used to define the new unit w.r.t. base unit.
639
+ offset : float
640
+ Zero offset for new unit.
641
+ comment : str
642
+ Optional comment to describe unit.
643
+ """
644
+ if isinstance(baseunit, str):
645
+ baseunit = _find_unit(baseunit)
646
+ # else, baseunit should be a instance of PhysicalUnit
647
+ # names, factor, powers, offset=0
648
+ unit = PhysicalUnit(
649
+ baseunit._names, baseunit._factor * factor, baseunit._powers, offset
650
+ )
651
+ unit.set_name(name)
652
+ if name in _UNIT_LIB.unit_table:
653
+ if (
654
+ _UNIT_LIB.unit_table[name]._factor != unit._factor
655
+ or _UNIT_LIB.unit_table[name]._powers != unit._powers
656
+ ):
657
+ raise KeyError(
658
+ f"Unit '{name}' already defined with different factor or powers."
659
+ )
660
+ _UNIT_LIB.unit_table[name] = unit
661
+ _UNIT_LIB.set("units", name, unit)
662
+ if comment:
663
+ _UNIT_LIB.help.append((name, comment, unit))
664
+
665
+
666
+ def add_unit(name, unit, comment=""):
667
+ """
668
+ Adding Unit.
669
+
670
+ Parameters
671
+ ----------
672
+ name : str
673
+ The name of the unit being added. For example: 'Hz'.
674
+ unit : str
675
+ Definition of the unit w.r.t. some other unit. For example: '1/s'.
676
+ comment : str
677
+ Optional comment to describe unit.
678
+ """
679
+ if comment:
680
+ _UNIT_LIB.help.append((name, comment, unit))
681
+ if isinstance(unit, str):
682
+ unit = eval(
683
+ unit,
684
+ {"__builtins__": None, "pi": pi}, # nosec: scope limited
685
+ _UNIT_LIB.unit_table,
686
+ )
687
+ unit.set_name(name)
688
+ if name in _UNIT_LIB.unit_table:
689
+ if (
690
+ _UNIT_LIB.unit_table[name]._factor != unit._factor
691
+ or _UNIT_LIB.unit_table[name]._powers != unit._powers
692
+ ):
693
+ raise KeyError(
694
+ f"Unit '{name}' already defined with different factor or powers."
695
+ )
696
+
697
+ _UNIT_LIB.unit_table[name] = unit
698
+ _UNIT_LIB.set("units", name, unit)
699
+
700
+
701
+ _UNIT_LIB = ConfigParser()
702
+
703
+
704
+ def _do_nothing(string):
705
+ """
706
+ Make the ConfigParser case sensitive.
707
+
708
+ Defines an optionxform for the units configparser that
709
+ does nothing, resulting in a case-sensitive parser.
710
+
711
+ Parameters
712
+ ----------
713
+ string : str
714
+ The string to be transformed for the ConfigParser
715
+
716
+ Returns
717
+ -------
718
+ str
719
+ The same string that was given as a parameter.
720
+ """
721
+ return string
722
+
723
+
724
+ _UNIT_LIB.optionxform = _do_nothing
725
+
726
+
727
+ def import_library(libfilepointer):
728
+ """
729
+ Import a units library, replacing any existing definitions.
730
+
731
+ Parameters
732
+ ----------
733
+ libfilepointer : file
734
+ New library file to work with.
735
+
736
+ Returns
737
+ -------
738
+ ConfigParser
739
+ Newly updated units library for the module.
740
+ """
741
+ global _UNIT_LIB
742
+ global _UNIT_CACHE
743
+ _UNIT_CACHE = {}
744
+ _UNIT_LIB = ConfigParser()
745
+ _UNIT_LIB.optionxform = _do_nothing
746
+
747
+ _UNIT_LIB.read_file(libfilepointer)
748
+
749
+ required_base_types = ["length", "mass", "time", "temperature", "angle"]
750
+ _UNIT_LIB.base_names = list()
751
+ # used to is_angle() and other base type checking
752
+ _UNIT_LIB.base_types = dict()
753
+ _UNIT_LIB.unit_table = dict()
754
+ _UNIT_LIB.prefixes = dict()
755
+ _UNIT_LIB.help = list()
756
+
757
+ for prefix, factor in _UNIT_LIB.items("prefixes"):
758
+ factor, comma, comment = factor.partition(",")
759
+ _UNIT_LIB.prefixes[prefix] = float(factor)
760
+
761
+ base_list = [0] * len(_UNIT_LIB.items("base_units"))
762
+
763
+ for i, (unit_type, name) in enumerate(_UNIT_LIB.items("base_units")):
764
+ _UNIT_LIB.base_types[unit_type] = i
765
+ powers = list(base_list)
766
+ powers[i] = 1
767
+ # print '%20s'%unit_type, powers
768
+ # cant use add_unit because no base units exist yet
769
+ _new_unit(name, 1, powers)
770
+ _UNIT_LIB.base_names.append(name)
771
+
772
+ # test for required base types
773
+ missing = [
774
+ utype for utype in required_base_types if utype not in _UNIT_LIB.base_types
775
+ ]
776
+ if missing: # pragma: no cover
777
+ raise ValueError(
778
+ "Not all required base types were present in the config file. missing: "
779
+ f"{missing}, at least {required_base_types} required."
780
+ )
781
+
782
+ _update_library(_UNIT_LIB)
783
+ return _UNIT_LIB
784
+
785
+
786
+ def update_library(filename): # pragma: no cover
787
+ """
788
+ Update units in current library from `filename`.
789
+
790
+ Parameters
791
+ ----------
792
+ filename : str or file
793
+ Source of units configuration data.
794
+ """
795
+ if isinstance(filename, str):
796
+ inp = open(filename, "rU")
797
+ else:
798
+ inp = filename
799
+ try:
800
+ cfg = ConfigParser()
801
+ cfg.optionxform = _do_nothing
802
+
803
+ # New in Python 3.2: read_file() replaces readfp().
804
+ if sys.version_info >= (3, 2):
805
+ cfg.read_file(inp)
806
+ else:
807
+ cfg.readfp(inp)
808
+
809
+ _update_library(cfg)
810
+ finally:
811
+ inp.close()
812
+
813
+
814
+ def _update_library(cfg): # pragma: no cover
815
+ """
816
+ Update library from :class:`ConfigParser` `cfg`.
817
+
818
+ Parameters
819
+ ----------
820
+ cfg : ConfigParser
821
+ ConfigParser loaded with unit_lib.ini data
822
+ """
823
+ retry1 = set()
824
+ for name, unit in cfg.items("units"):
825
+ data = [item.strip() for item in unit.split(",")]
826
+ if len(data) == 2:
827
+ unit, comment = data
828
+ try:
829
+ add_unit(name, unit, comment)
830
+ except NameError:
831
+ retry1.add((name, unit, comment))
832
+ elif len(data) == 4:
833
+ factor, baseunit, offset, comment = data
834
+ try:
835
+ add_offset_unit(name, baseunit, float(factor), float(offset), comment)
836
+ except NameError:
837
+ retry1.add((name, baseunit, float(factor), float(offset), comment))
838
+ else:
839
+ raise ValueError(f"Unit '{name}' definition {unit} has invalid format.")
840
+ retry_count = 0
841
+ last_retry_count = -1
842
+ while last_retry_count != retry_count and retry1:
843
+ last_retry_count = retry_count
844
+ retry_count = 0
845
+ retry2 = retry1.copy()
846
+ for data in retry2:
847
+ if len(data) == 3:
848
+ name, unit, comment = data
849
+ try:
850
+ add_unit(name, unit, comment)
851
+ retry1.remove(data)
852
+ except NameError:
853
+ retry_count += 1
854
+ else:
855
+ try:
856
+ name, factor, baseunit, offset, comment = data
857
+ add_offset_unit(name, factor, baseunit, offset, comment)
858
+ retry1.remove(data)
859
+ except NameError:
860
+ retry_count += 1
861
+ if retry1:
862
+ raise ValueError(
863
+ "The following units were not defined because they"
864
+ " could not be resolved as a function of any other"
865
+ " defined units: %s." % [x[0] for x in retry1]
866
+ )
867
+
868
+
869
+ _UNIT_CACHE = {}
870
+
871
+
872
+ def _find_unit(unit, error=False):
873
+ """
874
+ Find unit helper function.
875
+
876
+ Parameters
877
+ ----------
878
+ unit : str
879
+ str representing the desired unit
880
+ error : bool
881
+ If True, raise exception if unit isn't found.
882
+
883
+ Returns
884
+ -------
885
+ PhysicalUnit
886
+ The actual unit object
887
+ """
888
+ if isinstance(unit, str):
889
+ # Deal with 'as' for attoseconds
890
+ reg1 = re.compile(r"\bas\b")
891
+ unit = re.sub(reg1, "as_", unit)
892
+
893
+ name = unit.strip()
894
+ try:
895
+ unit = _UNIT_CACHE[name]
896
+ except KeyError:
897
+ try:
898
+ unit = eval(
899
+ name,
900
+ {"__builtins__": None}, # nosec: scope limited
901
+ _UNIT_LIB.unit_table,
902
+ )
903
+ except Exception:
904
+ # This unit might include prefixed units that aren't in the
905
+ # unit_table. We must parse them ALL and add them to the
906
+ # unit_table.
907
+
908
+ # First character of a unit is always alphabet or $.
909
+ # Remaining characters may include numbers.
910
+ regex = re.compile("[A-Z,a-z]{1}[A-Z,a-z,0-9]*")
911
+
912
+ unit_table = _UNIT_LIB.unit_table
913
+ prefixes = _UNIT_LIB.prefixes
914
+
915
+ for item in regex.findall(name):
916
+ item = re.sub(reg1, "as_", item)
917
+
918
+ # check if this was a compound unit, so each
919
+ # substring might be a unit
920
+ try:
921
+ eval(
922
+ item, {"__builtins__": None}, unit_table
923
+ ) # nosec: scope limited
924
+
925
+ except Exception: # maybe is a prefixed unit then
926
+ base_unit = item[1:].rstrip("_")
927
+
928
+ # check for single letter prefix before unit
929
+ if item[0] in prefixes and base_unit in unit_table:
930
+ add_unit(item, prefixes[item[0]] * unit_table[base_unit])
931
+
932
+ # check for double letter prefix before unit
933
+ elif item[0:2] in prefixes and item[2:] in unit_table:
934
+ add_unit(item, prefixes[item[0:2]] * unit_table[item[2:]])
935
+
936
+ # no prefixes found, unknown unit
937
+ else:
938
+ if error:
939
+ raise ValueError(f"The units '{name}' are invalid.")
940
+ return None
941
+
942
+ unit = eval(
943
+ name, {"__builtins__": None}, unit_table
944
+ ) # nosec: scope limited
945
+
946
+ _UNIT_CACHE[name] = unit
947
+ else:
948
+ name = unit
949
+
950
+ if not isinstance(unit, PhysicalUnit):
951
+ raise ValueError(f"The units '{name}' are invalid.")
952
+
953
+ return unit
954
+
955
+
956
+ def valid_units(unit):
957
+ """
958
+ Return whether the given units are vaild.
959
+
960
+ Parameters
961
+ ----------
962
+ unit : str
963
+ String representation of the units.
964
+
965
+ Returns
966
+ -------
967
+ bool
968
+ True for valid, False for invalid.
969
+ """
970
+ return _find_unit(unit) is not None
971
+
972
+
973
+ def unit_conversion(old_units, new_units):
974
+ """
975
+ Return conversion factor and offset between old and new units.
976
+
977
+ Parameters
978
+ ----------
979
+ old_units : str
980
+ Original units as a string.
981
+ new_units : str
982
+ New units to return the value in.
983
+
984
+ Returns
985
+ -------
986
+ (float, float)
987
+ Conversion factor and offset.
988
+ """
989
+ return _find_unit(old_units, error=True).conversion_tuple_to(
990
+ _find_unit(new_units, error=True)
991
+ )
992
+
993
+
994
+ def convert_units(val, old_units, new_units=None):
995
+ """
996
+ Take a given quantity and return in different units.
997
+
998
+ Parameters
999
+ ----------
1000
+ val : float
1001
+ Value in original units.
1002
+ old_units : str or None
1003
+ Original units as a string or None.
1004
+ new_units : str or None
1005
+ New units to return the value in or None.
1006
+
1007
+ Returns
1008
+ -------
1009
+ float
1010
+ Value in new units.
1011
+ """
1012
+ if not old_units or not new_units: # one side has no units
1013
+ return val
1014
+
1015
+ old_unit = _find_unit(old_units, error=True)
1016
+ new_unit = _find_unit(new_units, error=True)
1017
+
1018
+ (factor, offset) = old_unit.conversion_tuple_to(new_unit)
1019
+ return (val + offset) * factor
1020
+
1021
+
1022
+ def simplify_unit(old_unit_str):
1023
+ """
1024
+ Simplify unit string using built-in naming method.
1025
+
1026
+ Unit string 'ft*s/s' becomes 'ft'.
1027
+
1028
+ Parameters
1029
+ ----------
1030
+ old_unit_str : str
1031
+ Unit string to simplify.
1032
+ msginfo : str
1033
+ A string prepended to the ValueError which is raised if the units are invalid.
1034
+
1035
+ Returns
1036
+ -------
1037
+ str
1038
+ Simplified unit string.
1039
+ """
1040
+ if old_unit_str is None:
1041
+ return None
1042
+
1043
+ found_unit = _find_unit(old_unit_str, True)
1044
+
1045
+ new_str = found_unit.name()
1046
+ if new_str == "1":
1047
+ # Special Case. Unity always becomes None.
1048
+ new_str = None
1049
+
1050
+ # Restore units 'as' (attoseconds).
1051
+ if new_str:
1052
+ reg1 = re.compile(r"\bas_\b")
1053
+ new_str = reg1.sub("as", new_str)
1054
+ return new_str
1055
+
1056
+
1057
+ # Load in the default unit library
1058
+ file_path = open(os.path.join(os.path.dirname(__file__), "unit_library.ini"))
1059
+ with file_path as default_lib:
1060
+ import_library(default_lib)