🏛️ Bundle 2023-07
Quick start with Dessia SDK

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 offers 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 (PhysicalObject) or not (DessiaObject), and each object will have a specific representation in the platform, which we call its display.

For example, an object that inherits from DessiaObject can have a display 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 an object 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 constructed.

Each user will then have to write their own objects 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 performed. Then you can add all the specific methods to the object.

For example, to write an object describing a bearing, it will be necessary to:

  1. List all its attributes (inner diameter, outer diameter, thickness, number of balls, etc.).
  2. 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, the bearing object 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 object. 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)

Scheme

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

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, whether it be for a float, string, list, or array.

Thus, it is necessary to define the types of:

  • All attributes present in the constructor method “__init__”
  • All attributes present in the other methods of the object, 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, with a default value of an empty string. The Bearing object requires the definition of three float type attributes, a string type name, and a Ball object 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)

The declaration of the type is inserted after the attribute name by using a colon ":". Then, 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
  • List or tuple of float, integer, boolean or string
  • Dictionary with a string key and a built-in Python value
  • List or tuple of objects inheriting from DessiaObject or PhysicalObject
  • Union of objects inheriting from DessiaObject or PhysicalObject
  • Generic file.

To type Python built-ins, we use "float" for floats, "int" for integers, "bool" for booleans, and "str" for strings. To type a list, we use "List" followed by the type name in square brackets for the elements in the list. For a tuple, we use "Tuple" also followed by square brackets to define the type of the elements in the tuple (it is necessary to define the type of each element in the tuple using a comma, as shown in the following example). The use of "List" and "Tuple" requires importing these functions from the "typing" package. Here's an example using these different types:

from typing import List, Tuple
 
class Bearing(PhysicalObject):
 
    def __init__(self, balls: List[float],
								 positions: Tuple[float, float, float],
								 check: bool = True,
					       diameter: float = 0.01,
                 name: str = ""):

To type a Python dictionary, it is necessary to use "Dict", which is also imported from the "typing" package. For example, to type the following dictionary: { 'key1': 12.3, 'key2': 3.4 ... }, we use the syntax "Dict[str, float]".

To type a list or tuple of DessiaObject or PhysicalObject objects, we use the syntax "List[Ball]" if we want to declare a list of "Ball" objects. Similarly, we use "Tuple[Ball, Bearing]" for a tuple whose first element is a "Ball" object and the second element is a "Bearing" object.

It is possible to type a union in cases where the exact type of the expected object is unknown. For example, by typing "Union[Ball, Bearing]", we define an attribute that can be either a "Ball" object or a "Bearing" object. Unions are not considered for built-in Python types. In such cases, we add multiple different attributes to handle this scenario.

If we want to type a file, several options are available:

  • "BinaryFile" if we do not know the exact file extension
  • "StringFile" if it is a text file with UTF-8 encoding and no specific extension. If we want to force the extension to be "txt", we can use "TextFile".
  • "XLSXFile" if it is an Excel file with the extension "xlsx"
  • "XLSFile" if it is an Excel file with the extension "xls"
  • "XLSMFile" if it is an Excel file with the extension "xlsm"
  • "CSVFile" if it is a "csv" file
  • "MarkdownFile" if it is a markdown file
  • "JsonFile" if it is a text file with the extension "json"

All of these types are present in the "files" module of dessia_common, so it is important to import them in the following manner: "from dessia_common.files import BinaryFile"

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 XLSXFile
from 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']

Using a "classmethod" allows not to register the Excel file in the Dessia platform's database.

4. Object equality

The library "dessia_common" offers various concepts of equality between objects inheriting from "DessiaObject" or "PhysicalObject". For example, if we have two instances "ball1" and "ball2" of the class "Ball" and we want to test the equality between the two objects, different approaches are possible. We can test for equality based on the object data: we compare the constructor attributes of the two objects with each other, and the two objects are declared equal if the attributes are equal. Alternatively, we can test for equality based on the memory address of the objects, where two objects will be equal if they have the same memory address.

The generic objects of "dessia_common" offer a class attribute "_eq_is_data_eq" which transparently manages this equality behavior. If we choose "_eq_is_data_eq=True", then when we use the "==" operator between two objects, it will compare based on data. Otherwise, the memory address will be taken into account.

If we want to use equality based on data but exclude certain attributes, it is possible to use the class attribute "_non_data_eq_attributes" and define the list of attributes to exclude. For example, for the class "Bearing", if we want to exclude the "check" and "name" attributes from the equality calculation, we must structure the object as follows:

class Bearing(PhysicalObject):
	  _eq_is_data_eq = True
		_non_data_eq_attributes = ['check', 'name']
 
    def __init__(self, balls: List[float],
								 positions: Tuple[float, float, float],
								 check: bool = True,
					       diameter: float = 0.01,
                 name: str = ""):

It is also possible to rewrite the equality operator between two objects by defining the "_data_eq" method and following the following syntax:

class Ball(DessiaObject):
    _standalone_in_db = False
 
    def __init__(self, diameter: float,
                 name: str = ""):
        self.diameter = diameter
        DessiaObject.__init__(self, name=name)
 
    def _data_eq(self, other_object) -> bool:
        eq_ = True
        if other_object.__class__.__name__ != self.__class__.__name__:
            return False
        if self.diameter != other_object.diameter:
            return False
 
        return eq_

In Python, when using the "==" operator between two objects, a preliminary operation based on the hash of the objects is performed. Using the example of "ball1" and "ball2", two instances of the "Ball" class: when checking for equality between these two objects, Python first computes the hash of the two objects. If the hashes are different, then the objects are not equal and the "_data_eq" method is not executed. If the hashes are the same, then the "_data_eq" method is executed to check for equality between the two objects. This approach speeds up equality testing between objects by using a hash computation that is much faster than the computation of "_data_eq".

By default, "dessia_common" offers a hash computation that takes into account all of the object's attributes. The computation must be simple in order to be fast. Personalization is possible through the class attribute "_non_data_hash_attributes", where, in the same way as for equality, we can list the attributes that we want to exclude from the hash computation.

If the user wishes, it is also possible to rewrite the hash computation using the "__hash__" method. The following example provides a specific hash computation for the "Bearing" object: "

class Bearing(PhysicalObject):
	  _eq_is_data_eq = True
		_non_data_eq_attributes = ['check', 'name']
 
    def __init__(self, balls: List[float],
								 positions: Tuple[float, float, float],
								 check: bool = True,
					       diameter: float = 0.01,
                 name: str = ""):
 
    def __hash__(self):
				_hash = hash(self.diameter)
				for ball in self.balls:
						_hash += hash(ball)
        return _ha

Choosing the right "hash" is important because if they are all equal for an object (for example, for Bearing objects), then when searching for an instance in the database, we will quickly conclude that all Bearing objects in the database have the same "hash" and will therefore calculate the "_data_eq" method for all objects pairwise. All of these operations can significantly slow down the platform's performance.

To ensure the robustness of the object "hashes" present on the platform, it is possible to have a global view of the consistency of object "hashes" from the admin/hash-warnings tab (see documentation on platform usage). Here is a screenshot of this tab, and we can see that the "Car" object has poor hash diversity while the "Dataset" object has good hash diversity.

Hash warnings

5. Displays

Every object inheriting from DessiaObject or PhysicalObject can be represented on the Dessia platform using a set of displays. For example, a display can be text, a graph, or even a 3D model. The generic objects in dessia_common offer a set of methods by default to define these different representations. More specifically, the available displays are:

  • Free text using the markdown formatting options
  • 2D graph
  • 2D diagram
  • 3D geometry
  • Workflow

PhysicalObject objects offer a default 3D geometric representation in addition to the previously introduced representations. In contrast, DessiaObject objects do not offer a 3D geometric representation by default. The dessia_common library provides a formalism for customizing each of these methods. In the following sections, we will detail the implementation of each of these methods."

a. Display 3D

Customizing the 3D display requires detailing the "volmdlr_primitives" method. This method relies on the 3D geometric modeler "volmdlr" developed by Dessia (see chapter on volmdlr, we only briefly detail the use of this library here). It is a Python library that allows describing a 3D CAD from objects inheriting from the generic objects of dessia_common. Therefore, all volmdlr CADs can be treated similarly to DessiaObject or PhysicalObject objects and be present in the Dessia platform.

The "volmdlr_primitives" method describes how we want to construct the 3D geometry and must return a list of basic volmdlr 3D primitives (see chapter on volmdlr). The following code presents the syntax to define a 3D for Bearing and Ball objects:

from dessia_common.core import PhysicalObject
import volmdlr as vm
from volmdlr import faces
import volmdlr.primitives3d as p3d
 
class Ball(PhysicalObject):
    _standalone_in_db = False
 
    def __init__(self, diameter: float,
                 name: str = ""):
        self.diameter = diameter
        PhysicalObject.__init__(self, name=name)
 
    def volmdlr_primitives(self):
        primitives = []
        center = vm.Point3D(0, 6.25, 0)
        sphere = p3d.Sphere(center=center, radius=self.diameter / 2)
        primitives.append(sphere)
        return primitives
 
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)
 
		def volmdlr_primitives(self):
 
        primitives = []
        center = vm.Point2D(0, 0)
        external_circle = vm.wires.Circle2D(center=center, radius=self.external_diameter/2)
        internal_circle = vm.wires.Circle2D(center=center, radius=self.internal_diameter / 2)
 
        bearing_contour = faces.PlaneFace3D(faces.Plane3D(vm.OXYZ), faces.Surface2D(external_circle, [internal_circle]))
        primitives.append(bearing_contour)
        primitives.extend(self.ball.volmdlr_primitives())
 
        return primitives

For example, to define the 3D representation of the Ball object, we use the Sphere object from the primitives3d module of volmdlr to declare an elementary primitive constituting a sphere. This primitive will be returned in a list to conform to the dessia_common format. When this object is opened from the Dessia platform, a 3D viewer will be integrated into the object's display to represent its geometry.

b. Display 2D

Two types of 2D display are possible: either a 2D diagram or a set of 2D graphs. Both display modes require the use of the "plot_data" library developed by Dessia. This is a Python library that is structured in a similar way to "volmdlr" and whose components are decomposed from Python objects inheriting from DessiaObject. Similarly, all objects can be stored from the Dessia platform. For more details on the "plot_data" library, please refer to the 2D graphics chapter.

2D display from the "plot_data" library requires custom objects to implement the "plot_data()" method. When this method is present, the Dessia platform will offer a graphical representation in the object's display area. It is important to realize that the term "plot_data" is used to define 3 different concepts:

  • Name of the 2D representation library
  • Name of the method allowing an object inheriting from DessiaObject to represent a 2D diagram or graph in its display
  • Name of the file format allowing to write all components of the plot_data library in a generic way

For ease of use, the volmdlr library provides methods for generating these plot_data objects for its 2D objects (which are required for 2D plotting). For more details, it is necessary to refer to the plot_data documentation.

If you wish to create a 2D diagram, the "plot_data()" method of your customize object must return a list of "PrimitiveGroup" objects. This is a container object present in the plot_data library and contains a list of plot_data objects describing the diagram to be represented. The following code represents this method for generating a 2D representation of Ball and Bearing objects:

from dessia_common.core import PhysicalObject
import volmdlr as vm
from volmdlr import faces, wires
import plot_data
import volmdlr.primitives3d as p3d
 
class Ball(PhysicalObject):
    _standalone_in_db = False
 
    def __init__(self, diameter: float,
                 name: str = ""):
        self.diameter = diameter
        PhysicalObject.__init__(self, name=name)
 
    def plot_data(self):
        plot_datas = []
        center = vm.Point2D(0, 6.25)
        circle = wires.Circle2D(center=center, radius=self.diameter / 2)
        plot_datas.append(circle.plot_data())
 
        return [plot_data.PrimitiveGroup(primitives=plot_datas)]
 
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)
 
    def plot_data(self):
        plot_datas = []
        center = vm.Point2D(0, 0)
 
        external_circle = vm.wires.Circle2D(center=center, radius=self.external_diameter/2)
        plot_datas.append(external_circle.plot_data())
 
        internal_circle = vm.wires.Circle2D(center=center, radius=self.internal_diameter/2)
        plot_datas.append(internal_circle.plot_data())
 
        plot_datas.extend(self.ball.plot_data()[0].primitives)
 
        return [plot_data.PrimitiveGroup(primitives=plot_datas)]

For the Ball object, we start by using the volmdlr library to define the 2D geometric shapes. Then, from the "plot_data()" method of volmdlr objects, we generate these generic plot_data objects that can be used to fill PrimitiveGroups.

If you wish to include a scatter plot in the plot_data method, here is an example of a possible data input:

from dessia_common.core import PhysicalObject
import random
from plot_data import colors
import plot_data.core as plot_data
 
class Graph(PhysicalObject):
 
    def __init__(self, diameter: float,
                 name: str = ""):
        self.diameter = diameter
        PhysicalObject.__init__(self, name=name)
 
    def plot_data(self):
        elements = []
				SHAPES = ['round', 'square', 'triangle', 'ellipse']
				COLORS = [colors.RED, colors.BLUE, colors.GREEN, colors.YELLOW, colors.ORANGE, colors.VIOLET]
				for i in range(50):
				    random_shape = SHAPES[random.randint(0, len(SHAPES) - 1)]
				    random_color = COLORS[random.randint(0, len(SHAPES) - 1)]
				    elements.append({'mass': random.uniform(0, 50),
				                     'length': random.uniform(0, 100),
				                     'shape': random_shape,
				                     'color': random_color})
 
				scatterplot = plot_data.Scatter(elements=elements,
				                                x_variable='mass', y_variable='length')
			  return [scatterplot]

A large number of different graphs are possible and detailed in the "plot_data" section of the documentation.

c. Display markdown

To include free text in the display, it is possible to write a "to_markdown" method in custom objects that inherit from DessiaObject or PhysicalObject. The "to_markdown" method returns a string formatted according to the markdown standard (# for a Heading 1, ## for a Heading 2, etc.). It is necessary to insert the '\n' character into the string when a line break is desired.

The following code presents an example of how to apply this to the Bearing object:

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)
 
    def to_markdown(self):
        infos = ''
        infos += '## Bearing Infos \n\n'
        infos += '|name|height|external_diameter|internal_diameter|' + '\n'
        infos += '|:---------------:|:---------------:|:---------------:|:-----------------:|' + '\n'
        infos += '|' + self.name + '|' + str(self.height) + '|' + str(self.external_diameter) \
                 + '|' + str(self.internal_diameter) + '|\n'
 
        return infos

d. Display customization

It is possible to customize the displays of an object that inherits from DessiaObject or PhysicalObject. Broadly speaking, an object's display on the platform is controlled by a list of DisplaySetting objects (in the dessia_common displays module). Each DisplaySetting describes an elementary display such as plot_data, 3D, or markdown. The 'display_settings()' method is used to display this list of DisplaySetting objects.

In : ball1.display_settings()
 
Out : [<dessia_common.displays.DisplaySetting at 0x147c6ac70>,
	   <dessia_common.displays.DisplaySetting at 0x144f51c10>,
	   <dessia_common.displays.DisplaySetting at 0x144f51730>]

By default, the 'display_settings()' method returns a list of elementary displays whose methods are present in the object. For example, if the 'plot_data', 'primitives_volmdlr', and 'to_markdown' methods are present in the object, then the 'display_settings()' method will return a list of three DisplaySetting objects, where the first describes the 'plot_data' display, the second the 3D display, and the third the markdown display.

If you wish to add elements, you simply need to include the 'display_settings()' method in your object in order to overwrite the default method and include the DisplaySetting elements you desire (such as multiple 'plot_data' displays). The following code provides a modification of a 'display_settings()' method that aims to display multiple plot_data, markdown, and 3D displays.

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)
 
	def plot_data1(self):
		...
 
	def plot_data2(self):
		...
 
	def to_markdown1(self):
		...
 
	def to_markdown2(self):
		...
 
	def primitives_volmdlr1(self):
		...
 
	def primitives_volmdlr2(self):
		...
 
	def volmdlr_volume_model1(self):
		primitives = self.primitives_volmdlr1()
		return volmdlr.core.VolumeModel(primitives)
 
	def volmdlr_volume_model2(self):
		primitives = self.primitives_volmdlr2()
		return volmdlr.core.VolumeModel(primitives)
 
	def display_settings(self):
		display1 = DisplaySetting(selector='cad_1', type_='babylon_data',
                                  method='volmdlr_volume_model1().babylon_data',
								  serialize_data=True)
		display2 = DisplaySetting(selector='cad_2', type_='babylon_data',
								  method='volmdlr_volume_model2().babylon_data',
								  serialize_data=True)
		display3 = DisplaySetting(selector="markdown_1", type_="markdown",
								  method="to_markdown1")
		display4 = DisplaySetting(selector="markdown_2", type_="markdown",
								  method="to_markdown2")
		display5 = DisplaySetting(selector="plot_data_1", type_="plot_data",
								  method="plot_data1", serialize_data=True)
		display6 = DisplaySetting(selector="plot_data_2", type_="plot_data",
								  method="plot_data2", serialize_data=True)
		return [display1, display2, display3, display4, display5, display6]

It is important to follow the formalism for declaring DisplaySetting objects, as detailed in the previous example. The 'display_settings()' method will then be called by the '_displays()' method, which generates a JSON display that will be sent via the API to the platform.

6. Export customization

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)
 
		def to_txt(self, file_path: str):
        if not file_path.endswith('.md'):
            file_path += '.md'
        with open(file_path, 'w', encoding='utf-8') as file:
            self.to_txt_stream(file)
 
    def to_txt_stream(self, stream: dcf.StringFile):
        stream.write(self.to_markdown())
 
		def to_html(self, file_path: str):
        if not file_path.endswith('.html'):
            file_path += '.html'
        with open(file_path, 'w', encoding='utf-8') as file:
            self.to_html_stream(file)
 
    def to_html_stream(self, stream: dcf.StringFile):
        model = self.volmdlr_volume_model()
        babylon_data = model.babylon_data()
        script = model.babylonjs_script(babylon_data)
        stream.write(script)

7. Serialization and deserialization of objects

The dessia_common library provides methods for serializing and deserializing Python objects that inherit from either DessiaObject or PhysicalObject. Serialization transforms a Python object into a JSON file, while deserialization performs the inverse operation.

The benefits of these methods are twofold. Firstly, they allow for the transfer of Python objects through the Dessia API. Secondly, they enable the storage of Python objects in a database. Serialization is performed using the "to_dict()" method. For example, in the case of bearing, the resulting JSON takes the following form.

json = {'object_class': '__main__.Bearing',
	    'name': '',
	    'ball': {'object_class': '__main__.Ball', 'name': '', 'diameter': 0.01},
	    'internal_diameter': 0.1,
	    'external_diameter': 0.13,
	    'height': 0.05,
	    '_references': {}}

The inverse operation of deserialization requires the use of a class method because the goal is to instantiate an object from a JSON file (the object has not yet been instantiated, hence the need to use a class method). The method that enables this operation is "dict_to_object()", and it is used in the following manner:

bearing1 = Bearing.dict_to_object(json)

These methods can be customized, but in the context of standard usage of the Dessia platform, it is not recommended to do so.