Quick start with Dessia SDK
This is a technical document that explains the use of the dessia_common library for engineers.
Dessia is a platform that
allows engineers to organize their knowledge in a similar way to how a
developer structures their code using an object-oriented language. The
platform offers a Python library named dessia_common
with the goal of
providing master objects that include the necessary fundamentals for
proper functioning within the Dessia environment. These objects have
been designed to provide all the methods necessary for proper
functioning within the Dessia cloud platform, including the conversion
of Python objects to JSON, conversion of JSON to a Python object, and
defining equality between two objects. The document explains how to
define object typing to enable the platform to automatically generate an
appropriate form, and how to use the _standalone_in_db
attribute to
define whether an object is registered on its own or not. Finally, the
document discusses the equality between objects and offers different
methods for defining object equality, including using the __hash__
method to compute a unique hash for each object.
1. Introduction
Dessia offers engineers a way to organize their knowledge in a similar way to how a developer structures their code using an object-oriented language. This approach aligns with the philosophy of MBSE, which uses the UML formalism (originally associated with object-oriented approaches) to provide architecture software for engineering. Other implementations of MBSE, such as the SysML formalism, exist, but they are a no-code approach that doesn't address the problems that Dessia tackles (such as CAD, exhaustive generation, and artificial intelligence).
Dessia provides a Python
library named dessia_common
with the goal of providing master objects
that include the necessary fundamentals for the proper functioning of
the Dessia platform. In other words, all objects developed within the
framework of Dessia must inherit from these master objects, which
incorporate all the methods necessary for recognizing your objects on
the Dessia platform. Specifically, these objects have been designed to
provide all the methods necessary for proper functioning within our
cloud platform.
There are two main master objects:
DessiaObject
: master object for all objects without a 3D representation.PhysicalObject
: master object for objects with a 3D representation.
These master objects natively integrate a set of methods to allow them to exist and be recognized by our platform. In summary, here are the main characteristics of these objects:
- Conversion of a Python object to JSON (necessary for using Dessia APIs).
- Conversion of JSON to a Python object.
- How to define equality between two objects.
- Presence of the object alone in our noSQL database.
- Generic method to customize the generation of 3D (in the case of
PhysicalObject
). - 2D representation method.
The philosophy of Dessia is to break down any system into elementary objects. Each object can have either a physical representation or not, and each object will have a specific representation in the platform, which we call its display.
For example, a class that inherits from DessiaObject
can have displays that includes the following elements:
- 2D conceptual representation (from the
plot_data
library), - Graphics and selection system (from the
plot_data
library), - Table,
- Free text detailing the object.
The graphical and 2D representations require the use of a specific formalism introduced in the "plot_data for dummies" section.
For a class that inherits from PhysicalObject
, it will be possible to add a 3D representation to it.
To do this, it will be necessary to detail from the "volmdlr for dummies" library how the 3D should be built.
Each user will then have to write their own classes by making them inherit from either PhysicalObject
or DessiaObject
.
Next, you will need to detail all the inputs of the object, which we call its attributes.
These elements should be detailed from the object constructor, and typing should be added.
Then you can add all the specific methods to the object.
For example, to write an class describing a bearing, it will be necessary to:
- List all its attributes (inner diameter, outer diameter, thickness, number of balls, etc.).
- List all the methods specific to bearings.
- Generation of its 3D representation.
- Calculation of static and dynamic loads.
- 2D representation of the bearing.
By respecting this formalism, an object instance of bearing will offer a display that includes its 2D diagram and 3D representation.
2. Basics
When declaring a PhysicalObject
type object, which represents a 3D model, the syntax is as follows:
from dessia_common.core import PhysicalObject
class Bearing(PhysicalObject):
_standalone_in_db = True
def __init__(self, internal_diameter: float,
external_diameter: float,
height: float,
name: str = ""):
self.internal_diameter = internal_diameter
self.external_diameter = external_diameter
self.height = height
PhysicalObject.__init__(self, name=name)
The first line of code imports the master PhysicalObject
object from the dessia_common library, specifically from the core module.
Each object has two different behaviors regarding the databases present on the Dessia platform:
- Either the object is registered on its own (
_standalone_in_db = True
) - Or the object is not registered on its own and will
be registered from the parent object that contains it (
_standalone_in_db = False
).
This is a class attribute, and its value should remain unchanged throughout the object's lifecycle.
In the previous example, it didn't make sense to define the Bearing object as not _standalone_in_db
because it wasn't included in a parent class.
Therefore, it's important to define higher-level objects as _standalone_in_db
, while contained objects can have either declaration.
If we revisit the example of the Bearing object and introduce a Ball object to represent a ball, here's the possible syntax.
from dessia_common.core import PhysicalObject
class Ball(PhysicalObject):
_standalone_in_db = False
def __init__(self, diameter: float,
name: str = ""):
self.diameter = diameter
PhysicalObject.__init__(self, name=name)
class Bearing(PhysicalObject):
_standalone_in_db = True
def __init__(self, ball: Ball,
internal_diameter: float,
external_diameter: float,
height: float,
name: str = ""):
self.ball = ball
self.internal_diameter = internal_diameter
self.external_diameter = external_diameter
self.height = height
PhysicalObject.__init__(self, name=name)
Declaring an object in _standalone_in_db
mode may take longer during the database insertion
phase as it requires breaking down the global object into sub-objects.
However, it's important to use this declaration if we intend to open
this sub-object independently on the platform or reuse this object for
other data settings.
It's important to note
that using this declaration may increase the time required for database
insertion. Therefore, it's recommended to consider the use case before
deciding whether to declare the object in _standalone_in_db
mode.
To gain a more detailed understanding of these mechanisms, it's essential to introduce the workings of the Dessia framework. In simple terms, our framework consists of three primary components:
- Python IDE (which incorporates the Dessia SDK libraries, including dessia_common)
- Cloud Instance (which is centered around a database and a Python emulator)
- Web Interface (preferably accessed via the Chrome browser)
Communication of objects
between the Python IDE and the Cloud Instance is facilitated through the
Dessia API (as detailed in the following section). Users can leverage
the Dessia library, dessia_api_client
to send objects to a Cloud
Instance or import objects from a Python IDE. Upon sending an object, it
is first converted into the JSON format, which serves as the standard
web exchange format. This process, referred to as object serialization,
is achieved through the to_dict
method available in all DessiaObject
and PhysicalObject objects.
Once the JSON file reaches
the Cloud Instance, the reverse process, known as object
deserialization, is performed through the dict_to_object
method (also
available in DessiaObject and PhysicalObject objects). A dedicated
section on this topic, Serialization and Deserialization of Objects,
provides further details.
3. Typing
Since Python is a dynamically-typed language and the Dessia platform utilizes forms for end users to input object attributes, it is necessary to introduce object typing to enable the platform to automatically generate an appropriate form.
Thus, it is necessary to define the types of:
- All attributes present in the constructor method
__init__
- All arguments present in other methods of the class, as well as any possible return values.
The __init__
method is the constructor for Python objects. In other words, it is the method that is executed when the object is instantiated.
If we consider the previous example of an object that represents a bearing, we can see that the constructor of the Ball object requires the definition of a diameter of type float and a name of type string, defaulted to an empty string. The Bearing object requires the definition of :
- 3 float attributes
- a attribute name (string)
- an object of type Ball (thus, the attribute will contain a Python Ball object).
from dessia_common.core import PhysicalObject
class Ball(PhysicalObject):
_standalone_in_db = False
def __init__(self, diameter: float,
name: str = ""):
self.diameter = diameter
PhysicalObject.__init__(self, name=name)
class Bearing(PhysicalObject):
_standalone_in_db = True
def __init__(self, ball: Ball,
internal_diameter: float,
external_diameter: float,
height: float,
name: str = ""):
self.ball = ball
self.internal_diameter = internal_diameter
self.external_diameter = external_diameter
self.height = height
PhysicalObject.__init__(self, name=name)
Type declarations are inserted after the attribute name by using a colon :
. A
default value can be defined by using the equal sign =
after the type
definition.
More generally, the Dessia_common library from Dessia offers a set of different types:
- Float, integer, boolean or string (builtin)
- List or tuple of float, integer, boolean or string
- Dictionary with a string key and a builtin Python value
- List or tuple of objects inheriting from DessiaObject or PhysicalObject
- Union of objects inheriting from DessiaObject or PhysicalObject
- Generic file.
3.1 Builtins
Name | Python type | Example of value | Import from |
---|---|---|---|
Float | float | 1.2 | - |
Integer | int | 3 | - |
Boolean | bool | True | - |
String | str | "foo” | - |
def my_function(my_float: float = 1.2, my_integer: int = 3,
my_boolean: bool = True, my_string: str = "foo")
3.2 Sequences
Name | Python type | Constraint | Import from |
---|---|---|---|
Homogeneous Sequence | list[T] | - Cannot have several different Types - No length constraint | - |
Heterogeneous Sequence | tuple[T, U, V] | - Can have several different types - Length is defined - Order is defined | - |
Homogeneous Array | list[list[T]] | - T must be “simple”, i.e a python builtin (int , float , str , bool ) | - |
Dynamic Dictionary | dict[str, T] | - Keys are always of type str - Values type (T) must be simple, i.e a python builtin ( int , float , str , bool ) | - |
class Bearing(PhysicalObject):
def __init__(self, balls: list[Ball], positions: tuple[float, float, float],
check: bool = True, diameter: float = 0.01, name: str = ""):
tuple
has not constraint over member types. i-th value type is not necessarily a builtin as shown in the example.
3.3 Custom Classes
General case
In order to type a variable as an instance of one of your custom classes, you can simply reference the class as a type.
def my_function(ball: Ball)
# Ball is an instance of the Ball class
Recursive typing and classes that are not yet fully defined
You might encounter one of the following cases.
# Case 1 : Recursive typing. You need to type a variable with the class itself.
# Let's take the exemple of a "generate" classmethod that returns an instance of the class.
class MyClass:
@classmethod
def generate_one(cls) -> MyClass:
# This return typing fails in current Python versions because MyClass if not yet fully defined.
# Your IDE should warn you about it.
return cls()
# Case 2 : Typing with a class that is not defined yet.
class ClassA:
def __init__(self, my_object: ClassB)
# my_object's typing fails because ClassB is defined further down the module.
self.my_object = my_object
class ClassB:
pass
In these cases simply add the following statement at the start of the package.
from __future__ import annotations
This will ensure that the classes can be referenced before they are fully defined.
Example :
from __future__ import annotations
class ClassA:
def __init__(self, my_object: ClassB):
self.my_object = my_object
class ClassB:
@classmethod
def generate_one(cls) -> ClassB:
return cls()
@classmethod
def generate_many(cls) -> list[ClassB]:
return [cls()]
... is a perfectly valid code in Python 3.9. In a further release of Python, the import statement won't be needed anymore, but it still remains unplanned.
You could use the ForwardRef
proxy to achieve the same result, but while working with dessia_common
its use is discouraged by Python devs themselves.
3.4 Unions
Name | Python type | Meaning | Constraint | Import from |
---|---|---|---|---|
Union | Union[T, U, V] | Value can be of type T, U or V | - T, U & V should be related for this typing to make sense. | typing |
InstanceOf | InstanceOf[T] | Value is of type “any subclass from T”, including T itself. | - T must derive from DessiaObject | dessia_common.typings |
3.5 Files
If we want to type a file, several options are available:
BinaryFile
for binary-like file objects,StringFile
if it is a text file with UTF-8 encoding and no specific extension. If we want to force the extension to betxt
, we can useTextFile
.XLSXFile
if it is an Excel file with the extensionxlsx
XLSFile
if it is an Excel file with the extensionxls
XLSMFile
if it is an Excel file with the extensionxlsm
CSVFile
if it is acsv
fileMarkdownFile
if it is a markdown fileJsonFile
if it is a text file with the extensionjson
These can be found in the files
module of dessia_common
.
For example, if we want to read an Excel file from a class method, we can use the following syntax (it is necessary to import the openpyxl
package to read Excel files):
from dessia_common.files import XLSXFilefrom openpyxl import load_workbook
class Catalog(DessiaObject):
_standalone_in_db = True
def __init__(self, name: str = ''):
DessiaObject.__init__(self, name=name)
@classmethod
def read_exel(cls, excel: XLSXFile):
excel_catalog.seek(0)
wb2 = load_workbook(excel_catalog, data_only=True)
sheet_workbook = wb2['catalog']
3.6 Enumerations
Enumeration types will give you the ability to pick a string value from a restricted amount of possible choices, defined by the code. In the form, a dropdown will be shown with allowed values :
Name | Python type | Meaning | Import from | Constraint |
---|---|---|---|---|
Literal | Literal[”a”, “b”, “c”] | Value is a string that is one of “a”, “b”, or “c” | typing | |
KeyOf | KeyOf[DICT] | Value is one of DICT ’s keys | dessia_common.typings | - DICT is a dictionary object of type dict[str, T] - DICT is a constant |
Example of typical usage, that will give the previously shown dropdown input in both case :
class EnumWithLiteral(DessiaObject)
""" A dummy class to show proper usage of Literal Type. """
def __init__(self, color: Literal["red", "green", "blue"] = "red", name: str = ""):
self.color = color
super().__init__(name=name)
COLORS = {"red": (255, 0, 0), "green": (0, 255, 0), "blue": (0, 0, 255)}
class EnumWithKeyOf(DessiaObject):
""" A dummy class to show proper usage of KeyOf Type. """
def __init__(self, color_name: KeyOf[COLORS], name: str = ""):
self.color_name = color_name
super().__init__(name=name)
@property
def color(self):
return COLORS[self.color_name]
Note that in this example, COLORS
is a dictionary that doesn’t comply with the constraint given in section 3.2.
Its type is dict[string, tuple[float, float, float])
.
Its values are “complex” (tuple[float, float, float]
) and not of simple builtin type.
This is because we never actually need to input its value in a platform form. It is only used as a internal variable in the code and, thus, does not need to comply with the previous constraints.
These constraints are only relevant if your a typing a class attribute or a function argument, which will likely be shown as a form input.
Nevertheless, we still advise you build your code with these constraints applied to a maximum of your objects, even internal ones, because they usually refer to good coding and structuring practices.