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.
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