Everything within the C3 AI Suite is managed through or exists as a C3.ai Type. C3.ai Types allow users to describe, process and interact with data and analytics. Broadly speaking, C3.ai Types are like Java classes, and include 'fields', 'methods', and an inheritance structure. Once you understand the structure of C3.ai Type, you will be able to write C3.ai Types to load data, run analytics, build and deploy machine learning models, and configure application logic, for your research project.
All C3.ai Types are defined in a .c3typ file, stored in 'src' directory of a C3.ai Package. (We'll get to C3.ai Packages a little later). A .c3typ file can only define a single C3.ai Type. The .c3type file name must match the name of the C3.ai Type being defined.
First, we will describe the C3.ai Package, to download for this tutorial. Then, we will discuss the syntax, special keywords, fields, methods, and inheritance structure of C3.ai Types. Finally, we will review some examples.
In this tutorial, we will use the lightbulbAD C3.ai package. To download the source code for this package, please follow the instructions available here: C3 lightbulbAD Package
To help familiarize yourself with the syntax for a C3.ai Type, let's look at how the 'SmartBulb' Type is defined in the lightbulbAD C3.ai Package:
/*
* Copyright 2009-2020 C3 (www.c3.ai). All Rights Reserved.
* This material, including without limitation any software, is the confidential trade secret and proprietary
* information of C3 and its licensors. Reproduction, use and/or distribution of this material in any form is
* strictly prohibited except as set forth in a written license agreement with C3 and/or its authorized distributors.
* This material may be covered by one or more patents or pending patent applications.
*/
/**
* A single light bulb capable of measuring various properties, including power consumption, light output, etc.
*/
entity type SmartBulb extends LightBulb mixes MetricEvaluatable, NLEvaluatable type key "SMRT_BLB" {
/**
* This bulb's historical measurements.
*/
bulbMeasurements: [SmartBulbMeasurementSeries](smartBulb)
/**
* This bulb's historical predictions.
*/
@db(order='descending(timestamp)')
bulbPredictions: [SmartBulbPrediction](smartBulb)
/**
* This bulb's latest prediction.
*/
currentPrediction: SmartBulbPrediction stored calc "bulbPredictions[0]"
/**
* This bulb's historical events.
*/
bulbEvents: [SmartBulbEvent](smartBulb)
/**
* The latitude of this bulb.
*/
latitude: double
/**
* The longitude of this bulb.
*/
longitude: double
/**
* The unit of measurement used for this bulb's light output measurements.
*/
lumensUOM: Unit
/**
* The unit of measurement used for this bulb's power consumption measurements.
*/
powerUOM: Unit
/**
* The unit of measurement used for this bulb's temperature measurements.
*/
temperatureUOM: Unit
/**
* The unit of measurement used for this bulb's voltage measurements.
*/
voltageUOM: Unit
/**
* A SmartBulb is associated to a {@link Fixture} through a SmartBulbToFixtureRelation.
*/
@db(order='descending(start), descending(end)')
fixtureHistory: [SmartBulbToFixtureRelation](from)
/**
* The current Fixture to which this bulb is attached.
*/
currentFixture: Fixture stored calc 'fixtureHistory[0].(end == null).to'
/**
* Method to determine the expected lumens of a light bulb
*/
expectedLumens: function(wattage: !decimal, bulbType: !string): double js server
/**
* Returns the life span of this smartBulb
*/
lifeSpanInYears: function(bulbId: string): double js server
/**
* Returns the average life span of all smartBulbs.
*/
averageLifeSpan: function(): double js server
/**
* Returns the id of the smart bulb with the shortest recorded life span to date.
*/
shortestLifeSpanBulb: function(): string js server
/**
* Returns the id of the smart bulb with the longest recorded life span to date.
*/
longestLifeSpanBulb: function(): string js server
}
At a glance, a .c3typ file has the following components:
At a high level, the basic syntax to define C3.ai Type is as follows (originally from official C3.ai Type documentation here: https://developer.c3.ai/docs/7.12.17/topic/mda-types
[remix, extendable] [entity] type TypeName [extends, mixes AnotherType] { /* comments */ [field declaration] [method declaration] } |
Everything within square brackets '[]' is optional.
C3.ai Resources on C3.ai Types:
A C3.ai Type definition is introduced with a series of keywords. These keywords tell the C3 AI Suite how to construct this type, store it internally, and whether it inherits fields and methods from other already defined C3.ai Types. We describe each:
To learn more about C3.ai Type Definitions, please see the C3.ai resources here:
There are many categories of C3.ai Types.
We will cover the most commonly used categories in this tutorial:
Like many programming languages, the C3 AI Suite has primitives. Primitives define the units (or data types) of fields on a C3.ai Type (e.g., "int", "double", "float"). The C3 AI Suite includes a number of primitive, listed below for reference. Adding new primitives will require support from the C3.ai engineering team. However, most DTI researchers should be able to progress their research projects with the C3 AI Suite's existing primitives.
C3 AI Suite Primitives
To learn more about primitives, see the C3.ai Resources here:
The most common C3.ai Type you will define is a 'Persistable' type. Persistable types store data in a C3 AI Suite database. By default, all Persistable types are stored in Postgres. However, Persistable types can also be stored in file systems (e.g., Azure Blob) or key-value stores (e.g., Cassandra), by adding an annotation to your .c3typ file (as will be discussed below).
When defining a Persistable type, you MUST add the following two key terms to your .c3typ file
To learn more about 'Persistable' Types, please see the C3.ai Resources below:
Like a Java or C++ Class, C3.ai Types can be parameterized (or genericized). In fact, the C3 AI Suite uses the exact same syntax as Java and C++ to define a type's parameters (i.e., angle brackets '<>' ). When defining a Generic (Parametric) type, your Type name will be followed by angle brackets and a comma-separated list of parameters (usually other C3.ai Types). For example:
type TheType<V,U> { fieldA: V fieldB: [U] } |
Then, when using your Parametric Type in other places, you must specify the arguments for each parameter in the angle brackets. As example, you may define a new C3.ai Type with the following field:
type NewType { newField: TheType<TypeA, TypeB> } |
If your C3.ai types will be heavily re-used by other C3.ai developers, you should consider using Parametric (Generic) types.
To learn more about Generic Types, please see the C3.ai Resources here:
As discussed above, a C3.ai Type mainly consists of two components: fields and methods.
Fields are attributes or properties of a C3.ai Type. To define a field on C3.ai Type, use the following syntax: field name, followed by a colon ':', followed by a ValueType (e.g., "lumens: int"). By convention, field names are camelCase.
A C3.ai Type can have many different kinds of fields. Here are the four most commonly used fields.
Like many other programming languages, the C3 AI Suite has primitive fields (e.g., int, boolean, byte, double, datetime, string, longstring). Primitive fields are stored in a particular C3.ai type's database table.
Reference fields point (or refer) to other C3.ai Types. Reference fields link (or join) two C3.ai Types together. Under the covers, the Reference field stores a pointer (i.e., foreign key reference or ID) to record of another C3.ai Type. To define a Reference field, use the following syntax: field name, followed by colon ':', followed by Type Name (e.g., "building: Building").
Collection Fields contain a list of values or records. There are four categories of collections fields:
Collection fields can also be used to model one-to-many and many-to-many relationships between two C3.aiTypes. To do so, we sue the Foreign Key Collection Notation, which specifies the foreign key, by which two C3.ai Types (i.e., ThisType and AnotherType) are joined. To define this annotation, use the following syntax - "fieldName : `[AnotherType]` (fkey, key)" ; where (1) 'fkey' is a field on `AnotherType' to use as the foreign key; (2) 'key' is an optional field on ThisType, whose records should match those of the 'fkey' field on 'AnotherType' (defaults to `id` field of ThisType, if not specified). In other words, the Collection field will contain pointers to all records in AnotherType, where ThisType.key == AnotherType.fkey.
In shown in the example code and diagram below, the field bulbMeasurements, contains a list of all the SmartBulbMeasurementSeries records, where SmartBulb.id == SmartBulbMeasurements.smartBulb (e.g., a list of various measurements relevant to a SmartBulb, like temperature or power).
Please see the 'Foreign Key Collection and Schema Names" section in the following C3.ai Develop Documentation for more details: https://developer.c3.ai/docs/7.12.17/topic/mda-fields
Calculated fields are derived from other fields on a C3.ai Type. Calculated fields typically included either JavaScript code or C3.ai ExpressionEngineFunctions, and are specified as follows:
fieldName: FieldType [stored] calc "field_formula" |
There are two types of calculated fields:
At a high-level, methods are pieces of code that take in arguments or parameters and return new values. To define a method on a C3.ai Type, use the following syntax, method name followed by a colon ':' followed by a function signature, (e.g., function() or function(wattage: !decimal, bulbType: !string)), followed by a colon ':', followed by a return parameter, along with an implementation language and execution location.
Here's the syntax for a method's function signature:
function(parName1: [!] parType1, parName2: [!] parType2): |
We see the keyword 'function' followed by a comma-separated list of input parameters. To define an input parameter, use the following syntax - argument name followed by a colon ':', followed by argument type (e.g., bulbID: string). Optionally, you can also add an exclamation point '!' before the argument type to indicate the argument is required.
The function itself is NOT implemented in the .c3typ file; rather in a separate .js (JavaScript), .py (Python) or .r (R) files stored in the same directory as the .c3type file.
Following the function signature is a return parameter. Return parameters must be a C3.ai Type.
Finally, let's discuss the implementation language and execution location.
Language Specifications
Methods can be implemented in JavaScript, Python, or R.
Currently, these are the only three programming languages natively supported by the C3 AI Suite to define methods.
Execution Location
Methods can be executed in three locations:
Please note, R and Python methods can only be executed on the server. Javascript methods can be executed in either the server or client.
Additionally, for Python, we need to specify a valid 'ActionRuntime', for python function to be executed in. ActionRuntimes are defined in C3 AI Suite; new ActionRuntimes can be defined in a C3.ai Package, and are essentially conda environments. For example, 'server' will use the 'py-server' ActionRuntime.
As mentioned above, the function itself is defined in a different file from the .c3typ file. The name of this file should be the same as the .c3typ file, where the method is defined (e.g., SmartBulb.js and SmartBulb.c3typ), but have a different extension, per the function's implementation language (e.g., 'py' for Python, 'js' for JavaScript, 'r' for R). The function names in this file must exactly match the method names in the .c3typ file. For example, we define a C3.ai Type as suuch:
entity type NewType { field: double funcA: function(num: !int) : float js server funcB: function(num: !int, name: string) : double py server } |
This C3.ai Type will be defined in a file called 'NewType.c3typ'. We also need two other files, 'NewType.js' and 'NewType.py', in the same directory as 'NewType.c3typ'. Here's a peek inside:
'NewType.js'
function funcA(num) { ... } |
'NewType.py'
function funcB(num, name='default') { ... } |
Like many modern languages, Types can also inherit fields and methods from other types in an object oriented fashion. The keywords which signal inheritance are `mixes` and `extends`. These follow the name of the new type.
Where C3 Type inheritance differs from other languages, is that these two keywords signal two different types of inheritance.
Mix-ins
This most basic type of inheritance is signaled by the 'mixes' keyword. When a type 'mixes' another type, it inherits all of the fields and methods of that type. The new type gets its own schema and table, and the user can go on to use it as they normally would.
type ParentType { fieldA: int funcA: function(): ReturnType } type ChildType mixes ParentType { fieldB: double funcB: function(): ReturnType } |
In this example, ChildType also includes the field and method defined in the parent type.
Persistable Inheritance
This second type of inheritance is signaled by the 'extends' keyword. When a type 'extends' another type, it inherits its fields and methods, but also resides in the same schema and table as the original type. In fact, a type can only extend a type which has been marked as `extendable`.
extendable entity type ParentType schema name "PRT_TYP" { fieldA: int funcA: function(): ReturnType } entity type ChildType extends ParentType type key "CHLD" { fieldB: double funcB: function(): ReturnType } |
In this example, ParentType is an extendable Persistable type stored in schema 'PRT_TYP'. ChildType extends ParentType and uses type key 'CHLD'.
C3.ai developer resources on Inheritance:
Annotations can appear before Type definitions, field declarations, and method declarations. They do a variety of things such as configure the database technology used to store that Type, mark what type of ActionRuntime should be used to run a method, Mark a field or method as deprecated or beta, Specify a timeseries treatment, and specify parameters of C3 Analytics (discussion to come soon). These are only a selection of what annotations can do.
Here, we show a couple of examples:
/** * This bulb's historical predictions. */ @db(order='descending(timestamp)') bulbPredictions: [SmartBulbPrediction](smartBulb) |
The bulbPredictions field is annotated that the database should store the bulbPredictions in descending order by timestamp.
@ts(treatment='avg') lumens: double |
The lumens field of a type be default is now going to use the 'AVG' treatment.
C3.ai developer resources on Annotations:
Types, methods, and fields can be made 'final' using the 'final' keyword. This prevents fields and methods from being overridden by any type which mixes them in. If a type is made final, all fields and methods of that type are made final.
type FinalType { final method: function(): string } |
C3.ai developer resources on the final keyword
The C3 AI Suite also supports abstract types. If you use the keyword 'abstract' before the type definition, this will indicate an abstract type. You cannot instantiate an abstract type, and must mix it into another concrete type. This provides a great way to standardize interfaces. Here's an example:
abstract type Interace { field1: int method1: function(double): int } |
C3.ai developer resources on Abstract types:
Look through the lightbulbAD package and find .c3typ files. Look at these to see the range of possibilities