C3 AI Documentation Home

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
/**
 * 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 start and end timestamps 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 start date signified by the timestamp and continue until another replaces them. They also have a parametric from and to field to represent the relationship.

  • TimedIntervalRelation is a TimedRelation with both a start and end timestamp.

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:

Type
  /**
   * 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: ServiceAgreementStatus

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

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

Type
// 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 the characteristics and servicePointStatusSet fields containing only the values (single value only for servicePointStatus since 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 the characteristics and servicePointStatusSet fields containing only the values as they were at any point in time in the specified range.
  • Note that in both cases the field servicePointStatus is 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

Type
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 the servicePointMappings field containing only the mappings to service points that were valid at that time
  • MeterAsset.fetch({timeRange: TimeRange.make({start: '2014-01-01T00:00:00Z', end: '2015-01-01T00:00:00Z'})}) returns all meter assets with the values in the servicePointMappings field containing only the mappings to service points that were valid at any point in the specified time range
  • MeterAsset.tsEval({projection : "servicePointMappings.to.id", start: '2014-01-01T00:00:00Z', end: '2015-01-01T00:00:00Z', interval: "MONTH"}) will only return ServicePoint IDs where the ServicePointMeterAsset is 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.servicePointStatus in 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 ExpressionEngineFunction through 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 the tsDecl field, which explicitly define the start, end, value, and treatment (see TSDecl) to use when converting the stored data into a Timeseries.
Was this page helpful?