Skip to content

Commit f6456cf

Browse files
authored
Merge pull request #45 from ARKlab/feature/19532-unitofmeasure
ref(unitofmeasure): added unitofmeasure functionality
2 parents 0af79f1 + b8103d8 commit f6456cf

18 files changed

+471
-6
lines changed

README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,154 @@ To construct a GME Public Offer Extraction the following must be provided.
442442
443443
</table>
444444

445+
### Unit of Measure Conversion Functionality
446+
447+
### Overview
448+
449+
The unit of measure conversion functionality allows users to request a conversion of units for Market Data that was registered using a different unit. This feature is supported only for Actual and Versioned Time Series.
450+
Supported units are defined in the CommonUnitOfMeasure object and conform to ISO/IEC 80000 (i.e., `kW`, `MW`, `kWh`, `MWh`, `m`, `km`, `day`, `min`, `h`, `s`, `mo`, `yr`).
451+
452+
Note: Duration-based units are interpreted with the following fixed assumptions:
453+
`1 day = 24 hours`
454+
`1 mo = 30 days`
455+
`1 yr = 365 days`
456+
457+
Additional supported units include **currency codes** in 3-letter format as per ISO 4217:2015 (e.g., `EUR`, `USD`, `JPY`). These are not part of CommonUnitOfMeasure and must be specified as regular strings.
458+
Units of measure can also be **composite**, using the {a}/{b} syntax, where both {a} and {b} are either units from CommonUnitOfMeasure or ISO 4217 currency codes.
459+
460+
### Conversion Logic
461+
462+
Unit conversion is based on the assumption that each unit of measure can be decomposed into a **"BaseDimension"**, which represents a polynomial of base SI units (`m`, `s`, `kg`, etc.) and currencies (`EUR`, `USD`, etc.).
463+
A unit of measure is represented as a value in BaseDimension UnitOdMeasure.
464+
Example:
465+
10 `Wh` = 10 `kg·m²·s⁻³`
466+
Conversion is allowed when the BaseDimensions **match exactly**, i.e., the same set of base units raised to the same exponents.
467+
In Artesian, units that differ **only** in the **time dimension** are also potentially convertible, as the time dimension can be inferred from the data’s time interval.
468+
469+
### Example: Power to Energy Conversion
470+
471+
Converting `W` to `Wh`:
472+
`W` → BaseDimension: `k·m²·s⁻³`
473+
`Wh` → BaseDimension: `kg·m²·s⁻²`
474+
`1 h = 3600 s`
475+
**Conversion Steps:**
476+
10 W = 10 kg·m²/s³
477+
1 h = 3600 s
478+
10 kg·m²/s³ × 3600 s = 36000 kg·m²/s² = 10 Wh
479+
480+
### MarketData Registration with UnitOfMeasure
481+
482+
The UnitOfMeasure is defined during registration:
483+
484+
```Python
485+
mkd = MarketData.MarketDataEntityInput(
486+
providerName = "TestProviderName",
487+
marketDataName = "TestMarketDataName",
488+
originalGranularity=Granularity.Day,
489+
type=MarketData.MarketDataType.ActualTimeSerie,
490+
originalTimezone="CET",
491+
aggregationRule=AggregationRule.SumAndDivide,
492+
UnitOfMeasure = CommonUnitOfMeasure.kW
493+
)
494+
495+
registered = mkservice.readMarketDataRegistryByName(mkdid.provider, mkdid.name)
496+
if (registered is None):
497+
registered = mkservice.registerMarketData(mkd)
498+
```
499+
500+
### UnitOfMeasure Conversion and Aggregation Rule Override
501+
502+
In the QueryService, there are two supported methods related to unit of measure handling during extraction:
503+
504+
1. UnitOfMeasure Conversion
505+
2. Aggregation Rule Override
506+
507+
### UnitOfMeasure Conversion
508+
509+
To convert a UnitOfMeasure during data extraction, use the `.inUnitOfMeasure()` method. This function converts the data from the unit defined at MarketData registration to the target unit you specify in the query.
510+
511+
```Python
512+
qs = QueryService(cfg)
513+
data = qs.createActual() \
514+
.forMarketData([100011484]) \
515+
.inAbsoluteDateRange("2024-01-01","2024-01-02") \
516+
.inTimeZone("UTC") \
517+
.inGranularity(Granularity.Day) \
518+
.inUnitOfMeasure(CommonUnitOfMeasure.MW) \
519+
.execute()
520+
```
521+
522+
By default, the aggregation rule used during extraction is the one defined at registration. However, you can override it if needed. The conversion is always applied before aggregation.
523+
524+
### Aggregation Rule Override
525+
526+
AggregationRule can be overrided using the `.withAggregationRule()` method in QueryService.
527+
528+
```Python
529+
qs = QueryService(cfg)
530+
data = qs.createActual() \
531+
.forMarketData([100011484]) \
532+
.inAbsoluteDateRange("2024-01-01","2024-01-02") \
533+
.inTimeZone("UTC") \
534+
.inGranularity(Granularity.Day) \
535+
.withAggregationRule(AggregationRule.AverageAndReplicate) \
536+
.execute()
537+
```
538+
539+
Sometimes, especially when converting from a **consumption unit** (e.g., `MWh`) to a **power unit** (e.g., `MW`), the registered aggregation rule (e.g., `SumAndDivide`) may not make sense for the new unit.
540+
541+
If you **don’t override the aggregation rule**, the conversion may produce **invalid or misleading results**.
542+
543+
### Example: Convert power (`MW`) to energy (`MWh`):
544+
545+
```Python
546+
data = qs.createActual() \
547+
.forMarketData([100011484]) \
548+
.inAbsoluteDateRange("2024-01-01","2024-01-02") \
549+
.inTimeZone("UTC") \
550+
.inGranularity(Granularity.Day) \
551+
.inUnitOfMeasure(CommonUnitOfMeasure.MWh) \
552+
.withAggregationRule(AggregationRule.AverageAndReplicate) \
553+
.execute()
554+
```
555+
556+
### Composite Unit Example: `MWh/day`
557+
558+
```Python
559+
data = qs.createActual() \
560+
.forMarketData([100011484]) \
561+
.inAbsoluteDateRange("2024-01-01","2024-01-02") \
562+
.inTimeZone("UTC") \
563+
.inGranularity(Granularity.Day) \
564+
.inUnitOfMeasure(CommonUnitOfMeasure.MWh / CommonUnitOfMeasure.day) \
565+
.withAggregationRule(AggregationRule.AverageAndReplicate) \
566+
.execute()
567+
```
568+
569+
### CheckConversion: Validate Unit Compatibility
570+
571+
Use the `CheckConversion` method to verify whether a list of input units can be converted into a specifified target unit:
572+
573+
```Python
574+
from Artesian import ArtesianConfig, MarketData
575+
from Artesian.MarketData import CommonUnitOfMeasure
576+
577+
cfg = ArtesianConfg()
578+
579+
mkservice = MarketData.MarketDataService(cfg)
580+
581+
inputUnitsOfMeasure = [CommonUnitOfMeasure.kW, CommonUnitOfMeasure.kWh, "EUR/MWh"]
582+
targetUnitOfMeasure = CommonUnitOfMeasure.MW
583+
584+
checkConversionResult = mkservice.checkConversion(inputUnitsOfMeasure , targetUnitOfMeasure)
585+
```
586+
587+
**Returned Object: CheckConversionResult**
588+
589+
1. TargetUnitOfMeasure: "`kW`"
590+
2. ConvertibleInputUnitsOfMeasure: [ "`MW`", "`kW/s`" ]
591+
3. NotConvertibleInputUnitsOfMeasure: [ "`s`" ]
592+
445593
### Extraction Options
446594

447595
Extraction options for GME Public Offer queries.
@@ -518,6 +666,18 @@ Extraction options for GME Public Offer queries.
518666
.withPagination(1,10)
519667
```
520668

669+
#### UnitOfMeasure (for Actual and Versioned Time Series)
670+
671+
```Python
672+
.inUnitOfMeasure(CommonUnitOfMeasure.kWh)
673+
```
674+
675+
#### AggregationRule (for Actual and Versioned Time Series)
676+
677+
```Python
678+
.withAggregationRule(AggregationRule.SumAndDivide)
679+
```
680+
521681
## Write Data in Artesian
522682

523683
Using the MarketDataService is possible to register MarketData and write curves into it using the UpsertData method.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass(frozen=True)
5+
class CommonUnitOfMeasure:
6+
kW = "kW"
7+
MW = "MW"
8+
kWh = "kWh"
9+
MWh = "MWh"
10+
m = "m"
11+
km = "km"
12+
day = "day"
13+
min = "min"
14+
h = "h"
15+
s = "s"
16+
mo = "mo"
17+
yr = "yr"

src/Artesian/MarketData/MarketDataService.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ._Dto.ArtesianSearchResults import ArtesianSearchResults
1212
from ._Dto.MarketDataEntityInput import MarketDataEntityInput
1313
from ._Dto.MarketDataEntityOutput import MarketDataEntityOutput
14+
from ._Dto.CheckConversionResult import CheckConversionResult
1415
from ._Dto.UpsertData import UpsertData
1516
import asyncio
1617

@@ -396,6 +397,61 @@ def registerMarketData(
396397
self.registerMarketDataAsync(entity)
397398
)
398399

400+
async def checkConversionAsync(
401+
self: MarketDataService,
402+
inputUnitsOfMeasure: List[str],
403+
targetUnitOfMeasure: str
404+
) -> CheckConversionResult:
405+
"""
406+
Check UnitOfMeasure conversion.
407+
408+
Args:
409+
inputUnitsOfMeasure: the list of the input UnitOfMeasure to be check for
410+
conversion
411+
targetUnitOfMeasure: The target UnitOfMeasure
412+
413+
Returns:
414+
CheckConversionResult Entity (Async).
415+
"""
416+
url = "/uom/checkconversion"
417+
params = {"inputUnitsOfMeasure": inputUnitsOfMeasure,
418+
"targetUnitOfMeasure": targetUnitOfMeasure}
419+
with self.__client as c:
420+
res = await asyncio.gather(
421+
*[
422+
self.__executor.exec(
423+
c.exec,
424+
"GET",
425+
url,
426+
None,
427+
retcls=CheckConversionResult,
428+
params=params,
429+
)
430+
]
431+
)
432+
return cast(CheckConversionResult, res[0])
433+
434+
def checkConversion(
435+
self: MarketDataService,
436+
inputUnitsOfMeasure: List[str],
437+
targetUnitOfMeasure: str
438+
) -> CheckConversionResult:
439+
"""
440+
Check UnitOfMeasure conversion.
441+
442+
Args:
443+
inputUnitsOfMeasure: the list of the input UnitOfMeasure to be check for
444+
conversion
445+
targetUnitOfMeasure: The target UnitOfMeasure
446+
447+
Returns:
448+
CheckConversionResult Entity.
449+
"""
450+
451+
return _get_event_loop().run_until_complete(
452+
self.checkConversionAsync(inputUnitsOfMeasure, targetUnitOfMeasure)
453+
)
454+
399455
async def updateDerivedConfigurationAsync(
400456
self: MarketDataService,
401457
marketDataId: int,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from dataclasses import dataclass
2+
from typing import List
3+
4+
5+
@dataclass
6+
class CheckConversionResult:
7+
"""
8+
Class for the CheckConversionResult.
9+
10+
Attributes:
11+
targetUnitOfMeasure: the target UnitOfMeasure
12+
convertibleInputUnitsOfMeasure: the list of convertible input UnitOfMeasure
13+
notConvertibleInputUnitsOfMeasure: the list of not convertible input
14+
UnitOfMeasure
15+
"""
16+
17+
targetUnitOfMeasure: str
18+
convertibleInputUnitsOfMeasure: List[str]
19+
notConvertibleInputUnitsOfMeasure: List[str]

src/Artesian/MarketData/_Dto/MarketDataEntityInput.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Dict, List, Optional
33

44
from .DerivedCfg import DerivedCfg
5+
from .UnitOfMeasure import UnitOfMeasure
56
from .._Enum.DerivedAlgorithm import DerivedAlgorithm
67
from .._Enum import MarketDataType
78
from .._Enum import AggregationRule
@@ -34,6 +35,7 @@ class MarketDataEntityInput:
3435
originalGranularity: Granularity
3536
type: MarketDataType
3637
originalTimezone: str
38+
unitOfMeasure: Optional[UnitOfMeasure] = None
3739
derivedCfg: Optional[DerivedCfg] = None
3840
aggregationRule: AggregationRule = AggregationRule.Undefined
3941
tags: Optional[Dict[str, List[str]]] = None
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class UnitOfMeasure:
6+
"""
7+
Class for the UnitOfMeasure.
8+
9+
Attributes:
10+
value: the value of the unit of measure for the values of timeseries
11+
(actual and versioned)
12+
13+
"""
14+
15+
value: str

src/Artesian/MarketData/_Dto/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from .MarketDataEntityInput import MarketDataEntityInput
22
from .MarketDataEntityOutput import MarketDataEntityOutput
3+
from .CheckConversionResult import CheckConversionResult
4+
from .UnitOfMeasure import UnitOfMeasure
35
from .CurveRangeEntity import CurveRangeEntity
46
from .PagedResult import PagedResultCurveRangeEntity
57
from .ArtesianSearchResults import ArtesianSearchResults
@@ -31,4 +33,6 @@
3133
ArtesianMetadataFacet.__name__,
3234
ArtesianMetadataFacetCount.__name__,
3335
DerivedCfg.__name__,
36+
CheckConversionResult.__name__,
37+
UnitOfMeasure.__name__,
3438
] # type: ignore

src/Artesian/MarketData/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ..Granularity import Granularity
55
from ._Enum.MarketDataType import MarketDataType
66
from ._Enum.ArtesianMetadataFacetType import ArtesianMetadataFacetType
7+
from .CommonUnitOfMeasure import CommonUnitOfMeasure
78

89
from ._Dto import (
910
AuctionBids,
@@ -21,6 +22,8 @@
2122
ArtesianMetadataFacet,
2223
ArtesianMetadataFacetCount,
2324
DerivedCfg,
25+
CheckConversionResult,
26+
UnitOfMeasure,
2427
)
2528

2629
__all__ = [
@@ -44,5 +47,8 @@
4447
ArtesianMetadataFacetCount.__name__,
4548
ArtesianMetadataFacetType.__name__,
4649
DerivedCfg.__name__,
50+
CheckConversionResult.__name__,
4751
DerivedAlgorithm.__name__,
52+
CommonUnitOfMeasure.__name__,
53+
UnitOfMeasure.__name__,
4854
] # type: ignore

0 commit comments

Comments
 (0)