sunpeek-common-utils 0.1.0.dev6__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.
- sunpeek_common_utils/__init__.py +0 -0
- sunpeek_common_utils/errors.py +83 -0
- sunpeek_common_utils/helpers.py +151 -0
- sunpeek_common_utils/pydantic_models.py +66 -0
- sunpeek_common_utils/unit_uncertainty.py +497 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/METADATA +28 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/RECORD +12 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/WHEEL +4 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/licenses/AUTHORS.md +6 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/licenses/COPYING +674 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/licenses/COPYING.LESSER +165 -0
- sunpeek_common_utils-0.1.0.dev6.dist-info/licenses/NOTICES.md +186 -0
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
class SunPeekError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigurationError(SunPeekError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IncompatibleUnitError(SunPeekError):
|
|
10
|
+
"""Supplied unit (of raw sensor) is not compatible with the expected unit, e.g. as defined in SensorType.
|
|
11
|
+
"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VirtualSensorConfigurationError(SunPeekError):
|
|
16
|
+
"""Error in calcluation of virtual sensor due to missing input or input being None.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PCMethodError(SunPeekError):
|
|
22
|
+
"""General error in definition / configuration / calculation of PC method.
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CalculationError(SunPeekError):
|
|
28
|
+
"""General error in definition / handling of virtual senso.
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AlgorithmError(SunPeekError):
|
|
34
|
+
"""Error in some core_method algorithm.
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DuplicateNameError(SunPeekError):
|
|
40
|
+
"""Error due to creating a component with a duplicate name, where this is not allowed"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SensorNotFoundError(SunPeekError):
|
|
45
|
+
"""Error due to not finding a sensor when one was expected to exist"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SensorDataNotFoundError(SunPeekError):
|
|
50
|
+
"""Error due to not finding a data column for a sensor in the current data store"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NoDataError(SunPeekError):
|
|
55
|
+
"""No data are available in the selected data range"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TimeIndexError(SunPeekError):
|
|
60
|
+
"""Error handling or retrieving plant.time_index."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TimeZoneError(SunPeekError):
|
|
65
|
+
"""Error related to time zone"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DataProcessingError(SunPeekError):
|
|
70
|
+
"""Error related to data upload and processing"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DatabaseAlreadyExistsError(SunPeekError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CollectorDefinitionError(SunPeekError):
|
|
79
|
+
"""Error in Collector definition.
|
|
80
|
+
E.g. if supplied information is contradictory or not sufficient for full Collector definition.
|
|
81
|
+
See #70 for valid Collector definitions.
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sqlalchemy
|
|
3
|
+
from sqlalchemy import Column, Float, JSON, String
|
|
4
|
+
import sunpeek_common_utils.unit_uncertainty as uu
|
|
5
|
+
from sunpeek_common_utils.errors import ConfigurationError
|
|
6
|
+
from sunpeek_common_utils.unit_uncertainty import Q
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComponentParam:
|
|
11
|
+
"""Used to define parameters which are represented by Quantities, with optional limit checking.
|
|
12
|
+
|
|
13
|
+
Attributes
|
|
14
|
+
----------
|
|
15
|
+
unit: compatible unit
|
|
16
|
+
minimum: value of the parameters should not be below this
|
|
17
|
+
maximum: value of the parameters should not be above this
|
|
18
|
+
array_type: either "scalar" or "array", defaults to used to create the correct database column types and apply checks correctly.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, unit: str = None, minimum: float = -np.inf, maximum: float = np.inf, param_type: str = 'scalar'):
|
|
22
|
+
self.unit = unit
|
|
23
|
+
self.minimum = minimum
|
|
24
|
+
self.maximum = maximum
|
|
25
|
+
self.array_type = param_type
|
|
26
|
+
|
|
27
|
+
class AttrSetterMixin:
|
|
28
|
+
name = None
|
|
29
|
+
defer_post_config_changed_actions = False
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def define_component_attrs(cls):
|
|
33
|
+
for sub_cls in cls.all_subclasses():
|
|
34
|
+
# Get all ComponentParam from all component / subclass attributes
|
|
35
|
+
params = {attr: obj for attr, obj in sub_cls.__dict__.items() if isinstance(obj, ComponentParam)}
|
|
36
|
+
for attr, obj in params.items():
|
|
37
|
+
sub_cls.add_component_attr(attr, obj.unit, obj.minimum, obj.maximum, obj.array_type)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def add_component_attr(cls, name, unit=None, minimum=-np.inf, maximum=np.inf, array_type='scalar'):
|
|
41
|
+
if array_type == 'scalar':
|
|
42
|
+
setattr(cls, f'_{name}_mag', Column(Float))
|
|
43
|
+
elif 'array':
|
|
44
|
+
setattr(cls, f'_{name}_mag', Column(JSON))
|
|
45
|
+
setattr(cls, f'_{name}_unit', Column(String))
|
|
46
|
+
prop = property(fset=lambda cls, value: cls.set_component_attribute(name, value, array_type),
|
|
47
|
+
fget=lambda cls: cls.get_component_attribute(name))
|
|
48
|
+
setattr(cls, name, prop)
|
|
49
|
+
if not hasattr(cls, '_attr_props'):
|
|
50
|
+
cls._attr_props = {}
|
|
51
|
+
cls._attr_props[name] = {'unit': unit, 'minimum': minimum, 'maximum': maximum}
|
|
52
|
+
# print (self.attr_props[name])
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def all_subclasses(cls, c=None):
|
|
56
|
+
if c is None: c = cls
|
|
57
|
+
return set(c.__subclasses__()).union(
|
|
58
|
+
[s for c in cls.__subclasses__() for s in c.all_subclasses(c)])
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def register_callback(cls, callback_type, func):
|
|
62
|
+
print(cls)
|
|
63
|
+
try:
|
|
64
|
+
getattr(cls, callback_type).append(func)
|
|
65
|
+
except AttributeError:
|
|
66
|
+
cls.post_config_changed_callbacks = [func]
|
|
67
|
+
|
|
68
|
+
def _check_value(self, name, value, param_type):
|
|
69
|
+
unit = self._attr_props[name]['unit']
|
|
70
|
+
|
|
71
|
+
uu.assert_compatible(value.units, unit)
|
|
72
|
+
|
|
73
|
+
val = value.to(unit).magnitude
|
|
74
|
+
min = self._attr_props[name]['minimum']
|
|
75
|
+
max = self._attr_props[name]['maximum']
|
|
76
|
+
|
|
77
|
+
if param_type == 'scalar' or param_type is None:
|
|
78
|
+
if isinstance(value.magnitude, np.ndarray):
|
|
79
|
+
raise ConfigurationError("Attempting to assign an array quantity to a scalar type parameter")
|
|
80
|
+
if val < min:
|
|
81
|
+
raise ConfigurationError(
|
|
82
|
+
f"attempting to set a value for attribute {name} that is less than the minimum of {min}{unit}")
|
|
83
|
+
if val > max:
|
|
84
|
+
raise ConfigurationError(
|
|
85
|
+
f"attempting to set a value for attribute {name} that is greater than the maximum of {max}{unit}")
|
|
86
|
+
|
|
87
|
+
if param_type == 'array':
|
|
88
|
+
if not isinstance(value.magnitude, np.ndarray):
|
|
89
|
+
raise ConfigurationError("Attempting to assign an array quantity to a scalar type parameter")
|
|
90
|
+
if (val < min).any():
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
f"attempting to set a value for attribute {name} that is less than the minimum of {min}{unit}")
|
|
93
|
+
if (val > max).any():
|
|
94
|
+
raise ConfigurationError(
|
|
95
|
+
f"attempting to set a value for attribute {name} that is greater than the maximum of {max}{unit}")
|
|
96
|
+
|
|
97
|
+
def set_component_attribute(self, name, value, array_type):
|
|
98
|
+
if value is not None:
|
|
99
|
+
value = uu.parse_quantity(value)
|
|
100
|
+
if not self._attr_props[name]['unit'] == 'no_check':
|
|
101
|
+
self._check_value(name, value, array_type)
|
|
102
|
+
|
|
103
|
+
if array_type == 'scalar' or array_type is None:
|
|
104
|
+
mag = value.magnitude
|
|
105
|
+
elif array_type == 'array':
|
|
106
|
+
mag = value.magnitude.tolist()
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("type must be either 'scalar' or 'array'")
|
|
109
|
+
|
|
110
|
+
setattr(self, f'_{name}_mag', mag)
|
|
111
|
+
setattr(self, f'_{name}_unit', str(value.units))
|
|
112
|
+
else:
|
|
113
|
+
# setattr(self, name, None)
|
|
114
|
+
setattr(self, f'_{name}_mag', None)
|
|
115
|
+
setattr(self, f'_{name}_unit', None)
|
|
116
|
+
|
|
117
|
+
if getattr(self, 'plant', None) is not None and (self.__class__.__name__ != 'Sensor'):
|
|
118
|
+
# Update whether virtual sensor can be calculated with new plant information
|
|
119
|
+
if not self.defer_post_config_changed_actions:
|
|
120
|
+
[func(self.plant) for func in self.plant.post_config_changed_callbacks]
|
|
121
|
+
|
|
122
|
+
def get_component_attribute(self, name):
|
|
123
|
+
if self.__getattribute__(f'_{name}_mag') is None:
|
|
124
|
+
return None
|
|
125
|
+
return Q(self.__getattribute__(f'_{name}_mag'), self.__getattribute__(f'_{name}_unit'))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def get_default_unit(cls, name: str) -> str:
|
|
131
|
+
"""Return default unit of a class attribute defined as ComponentParam.
|
|
132
|
+
"""
|
|
133
|
+
return cls._attr_props[name]['unit']
|
|
134
|
+
|
|
135
|
+
def __str__(self):
|
|
136
|
+
try:
|
|
137
|
+
if self.name is not None:
|
|
138
|
+
return f'HarvestIT {self.__class__.__name__} component called {self.name}'
|
|
139
|
+
else:
|
|
140
|
+
return f'HarvestIT {self.__class__.__name__} object'
|
|
141
|
+
except sqlalchemy.exc.InvalidRequestError:
|
|
142
|
+
return "unknown SunPeek component"
|
|
143
|
+
|
|
144
|
+
def __repr__(self):
|
|
145
|
+
return self.__str__()
|
|
146
|
+
|
|
147
|
+
def __dir__(self):
|
|
148
|
+
try:
|
|
149
|
+
return list(self.sensor_slots.keys()) + list(super().__dir__())
|
|
150
|
+
except AttributeError:
|
|
151
|
+
return super().__dir__()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
2
|
+
from pydantic import validator, Field, AliasChoices
|
|
3
|
+
import pint
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
import numpy as np
|
|
6
|
+
import datetime as dt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseModel(PydanticBaseModel):
|
|
10
|
+
class Config:
|
|
11
|
+
from_attributes = True
|
|
12
|
+
arbitrary_types_allowed = True
|
|
13
|
+
fields = {
|
|
14
|
+
'class_': 'class'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@validator('*', pre=True)
|
|
18
|
+
def make_strings(cls, v):
|
|
19
|
+
if isinstance(v, pint.Unit):
|
|
20
|
+
v = str(v)
|
|
21
|
+
return v
|
|
22
|
+
# if isinstance(v, str):
|
|
23
|
+
# v = v.lower()
|
|
24
|
+
# return v
|
|
25
|
+
# elif isinstance(v, pint.Quantity):
|
|
26
|
+
# return str(v.units)
|
|
27
|
+
# elif isinstance(v, cmp.Collector):
|
|
28
|
+
# return v.name
|
|
29
|
+
return v
|
|
30
|
+
|
|
31
|
+
@validator('units', 'native_unit', pre=True, check_fields=False)
|
|
32
|
+
def validate_units(cls, v):
|
|
33
|
+
if isinstance(v, pint.Quantity):
|
|
34
|
+
return str(v.units)
|
|
35
|
+
return v
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def np_to_list(val):
|
|
39
|
+
if isinstance(val, np.ndarray) and val.ndim == 1:
|
|
40
|
+
return list(val)
|
|
41
|
+
elif isinstance(val, np.ndarray) and val.ndim > 1:
|
|
42
|
+
out = []
|
|
43
|
+
for array in list(val):
|
|
44
|
+
out.append(np_to_list(array))
|
|
45
|
+
return out
|
|
46
|
+
return val
|
|
47
|
+
|
|
48
|
+
class Quantity(BaseModel):
|
|
49
|
+
magnitude: float | List[float] | List[List[float]] = Field(validation_alias=AliasChoices('magnitude', 'Value'))
|
|
50
|
+
units: str = Field(validation_alias=AliasChoices('units', 'Unit'))
|
|
51
|
+
|
|
52
|
+
@validator('magnitude', pre=True)
|
|
53
|
+
def convert_numpy(cls, val):
|
|
54
|
+
return np_to_list(val)
|
|
55
|
+
|
|
56
|
+
@validator('units', pre=True)
|
|
57
|
+
def pretty_unit(cls, val):
|
|
58
|
+
if isinstance(val, pint.Unit):
|
|
59
|
+
return f"{val:~P}"
|
|
60
|
+
return val
|
|
61
|
+
|
|
62
|
+
@validator('units', pre=True)
|
|
63
|
+
def alternate_none(cls, val):
|
|
64
|
+
if val == '-':
|
|
65
|
+
return 'dimensionless'
|
|
66
|
+
return val
|