Understanding a Timed Values Relationship
Users can use certain Types to represent data that evolves over time, including time series data. Examples of data that can change over time are:
- Weather Data — Temperature, humidity, pressure, and wind speed all change continuously throughout the day.
- Population Data — Census data, including population size, and demographic details evolve over time.
- Economic Indicators — Macro-economic indicators like inflation rate, unemployment rate, and GDP growth change over time.
- Electricity Usage — The power consumption of a household or facility can fluctuate over the course of a day.
These Types can also represent relationships between two entities that change over time. For instance, foreign exchange rates, often referred to as currency exchange ratios, represent the value of one country's currency in relation to another. They fluctuate over time due to various factors including interest rates, inflation, and supply and demand in the currency exchange markets.
The relationship between the entities (two different currencies) is below:
/**
* Type to store currency foreign exchange ratios.
*/
@db(index+=['start', 'end'])
entity type CurrencyConversion mixes TimedIntervalRelation<Currency, Currency> schema name 'CUR_CONV' {
/**
* The date this conversion ratio was added to seed data.
* This is the latest time this conversion can be confirmed to be accurate.
*/
conversionRatioDate: datetime
/**
* The currency conversion ratio.
*/
conversionRatio: double
}TimedIntervalRelation ties a start and end timestamp to the relationship between the two currencies.
Overview of key Types
Key Types in the C3 Agentic AI Platform related to Timed Values are listed below:
Types that mixin TimedValue allow you to associate a timestamp (or
datetime) with a value. Used in conjunction with Types that mixin TimedValueHistory, it is possible to keep a history of what those values were over time. This can be useful for things like tracking when the status of an account was changed over its lifespan or such as tracking the number of people living in a house.TimedIntervalValue is similar to TimedValue but uses a time range instead of a single timestamp. These values have
startandendtimestamps associated with them. When performing a timed fetch, any arrays containing elements with temporal values can be filtered to return only values corresponding to the requested time or time range.TimedCharacteristic is similar to TimedValue but instead of just a value, it also has a name associated with each value.
TimedRelation represents relationships that have a
startdate signified by the timestamp and continue until another replaces them. They also have a parametricfromandtofield to represent the relationship.TimedIntervalRelation is a TimedRelation with both a
startandendtimestamp.
When to use timed Types instead of timeseries
Timed data types should be used when the data is sparse or changing at a slow rate over time.
Use timed data types for data that is infrequently changing or would not normally be thought of as a series, such as the square area of a house (may change as the house is remodeled, but often is unchanged), the owner of a pump, or the status of a meter (for a summer home, for example, it may only be active from May until September).
Use Timeseries for data that primarily represents values changing over time. This includes periodical measurements, such as every day or every month, or values that come in frequently but not necessarily periodically, such as temperature readings from a pump. These sort of data should have a natural normalization strategy (for example, divide the monthly value evenly into daily fractions, or use the last reading until the next comes in).
Setting up a timed value field
For example, you may have utility data with an account status that changes over time and you may be interested in maintaining the most recent value of the status on the account while also maintaining the history of changes to the status over time. To achieve this, you can create a field in your type that holds the TimedValue which is the latest value and keep it synchronized with the history by using an fkey field for the TimedValueHistory. The following example shows how this is done:
/**
* Status set fields
*
* Service Agreement status history in a descending order over time.
*/
@db(order='descending(timestamp)')
serviceAgreementStatusSet: [ServiceAgreementStatusSet](parent)
/**
* Latest status of service agreement.
*/
@db(timedValueHistoryField='serviceAgreementStatusSet')
serviceAgreementStatus: ServiceAgreementStatusIn this example, serviceAgreementStatus mixes in TimedValue and serviceAgreementStatusSet mixes in TimedValueHistory. The serviceAgreeStatus field holds the latest status for a serviceAgreement and, because of the db annotation specifying the timedValueHistoryField, that field can hold the history of statuses for the service agreement. With this configuration, when input data specifies a new value for serviceAgreementStatus, an entry is added to the history if need be and the serviceAgreementStatus is automatically maintained to hold the latest value.
serviceAgreementStatus has eventually consistent behavior. The computed value may not always reflect the current state based on the most recent entry in serviceAgreementStatusSet, depending on when the query is made. Factors that affect stored calc computation (such as disableAsyncProcessing in UpsertSpec or full task nodepool utilization) will also delay updates to serviceAgreementStatus.
Using TimedValue vs. TimedCharacteristic
When you have a specific attribute that is known at the time of data modeling, using TimedValue is preferable. This enables you to create individual fields for those values and track their history automatically.
This makes sense for things like an account history. If, however, there are many attributes describing something that can change over time, or those attributes are not all known at the time of data modeling, TimedCharacteristic is a better choice.
With TimedCharacteristic you can independently track the values of multiple attributes related to a parent without having to make any schema changes when new attributes must be tracked.
Examples
TimedValue
// TimedCharacteristicHistory mixes TimedValueHistory mixes TimedValue
entity type ServicePointCharacteristicSet mixes TimedCharacteristicHistory<ServicePoint>, String ...
// TimedValueHistory mixes TimedValue
entity type ServicePointStatusSet mixes TimedValueHistory<ServicePoint>, String...
extendable entity type ServicePoint extends LocationMeasurementDevice mixes FeatureEvaluatable type key 'SP' {
characteristics : [ServicePointCharacteristicSet](parent)
// Status set fields
@db(order = "descending(timestamp)")
servicePointStatusSet : [ServicePointStatusSet](parent)
// latest status (calculated)
@db(timedValuesField = "servicePointStatusSet")
servicePointStatus : ServicePointStatus
}A ServicePoint is a specific location at a premise where a utility company delivers consumable resources (electricity, gas, and water).
ServicePoint.fetch({asOf: '2014-01-01T00:00:00Z'})returns all service points with the values in thecharacteristicsandservicePointStatusSetfields containing only the values (single value only forservicePointStatussince there can only be one valid value at any point in time) as they were at that time.ServicePoint.fetch({timeRange: TimeRange.make({start: '2014-01-01T00:00:00Z', end: '2015-01-01T00:00:00Z'})}returns all service points with the values in thecharacteristicsandservicePointStatusSetfields containing only the values as they were at any point in time in the specified range.- Note that in both cases the field
servicePointStatusis null. That field is populated with the chronologically latest value only during non-timed fetch. ServicePoint.tsEval({projection : "servicePointStatusSet.value", start: '2014-01-01T00:00:00Z', end: '2015-01-01T00:00:00Z', interval: "MONTH"})returns just the values for the time range specified.
TimedRelation
entity type ServicePointMeterAsset mixes TimedIntervalRelation<ServicePoint, MeterAsset> ...
extendable entity type MeterAsset extends GridAsset type key 'METER' {
@db(order = "descending(start), descending(end)")
servicePointMappings : [ServicePointMeterAsset](to)
}MeterAsset.fetch({asOf: '2014-01-01T00:00:00Z'})returns all meter assets with the values in theservicePointMappingsfield containing only the mappings to service points that were valid at that timeMeterAsset.fetch({timeRange: TimeRange.make({start: '2014-01-01T00:00:00Z', end: '2015-01-01T00:00:00Z'})})returns all meter assets with the values in theservicePointMappingsfield containing only the mappings to service points that were valid at any point in the specified time rangeMeterAsset.tsEval({projection : "servicePointMappings.to.id", start: '2014-01-01T00:00:00Z', end: '2015-01-01T00:00:00Z', interval: "MONTH"})will only returnServicePointIDs where theServicePointMeterAssetis valid during that time range.
Nesting
Using the above definitions for ServicePoint and MeterAsset: MeterAsset.fetch({include: "servicePointMappings.from.servicePointStatusSet", asOf: '2014-01-01T00:00:00Z'}) returns all MeterAssets with only the servicePointMappings that were valid at that time. For each servicePointMapping returned it will bring back the service point and its status at that point in time (for example, the requested time range is applied across all references in the include spec).
Troubleshooting
The following caveats must be observed with this feature:
- Any non-array timed value field (for example,
ServicePoint.servicePointStatusin the above example) is null when using timed fetch. - Stored calculated fields based on timed value fields are returned with their stored value which is not expected to be relevant or valid in the timed fetch requested time range.
- Note that for Timeseries data, it may be accessed directly in SimpleMetric expressions (see also
ExpressionEngineFunctionthrough its normalized value, for example,myField.normalized.quantity). For timed data types though, the values must first be converted into a Timeseries. This conversion is achieved in SimpleMetrics by using thetsDeclfield, which explicitly define thestart,end,value, andtreatment(seeTSDecl) to use when converting the stored data into a Timeseries.
Related content
- To hold the other ref in latest TimedIntervalRelation or TimedRelation, review TimedIntervalRelationRef and TimedRelationRef
- To understand how to store historical values over for a time for a timed interval value, review TimedCharacteristicHistory, TimedIntervalValueHistory and TimedIntervalValueHistory