🏛️ Bundle 2023-07
Piping in constraint environment

Piping in constraint environment

1 - Introduction

The objective of this tutorial is to generate and optimize a pipe that is in a constraint environment. Indeed, we will force the pipe to pass through 4 points and force its neutral fiber minimal radius to be superior than an imposed value.

2 - Build the bot in 4 steps

Before beginning, you have access to tutorial5 in the tutorials folder cloned as explained in the first tutorial. We will apply DessIA method to find the best solution to our problem.

2.1 - Describe your engineering system

2.1.1 - Constraint environment

First of all, we need to import some packages :

import volmdlr as vm
import volmdlr.faces as vmf
import volmdlr.primitives3d as p3d
 
from dessia_common import DessiaObject
from typing import List

In our problem, we want to generate and optimize solutions regarding a constraint environment. In this case, we want to install a pipe with a housing, a support. We define this environment with Housing method that needs a list of faces (the environment) and the spatial environment origin.

class Housing(DessiaObject):
    _standalone_in_db = False
 
    def __init__(self, faces: List[vmf.Face3D], origin: vm.Point3D,
                 name: str = ''):
        self.origin = origin
        self.faces = faces
        DessiaObject.__init__(self, name=name)
 
    def volmdlr_primitives(self): #thanks to this method you could display 3D
        for face in self.faces:
            face.translation(self.origin, copy=False)
        return self.faces

How to create a face ?

Faces are created thanks to volmdlr which will be called thanks to 'vm.'.

  • Create a frame :
frame_center = vm.Point3D(0, 1, 0) #x,y,z position
dir1, dir2, dir3 = vm.Vector3D(1,0,0), vm.Vector3D(0,1,0), vm.Vector3D(0,0,1) #vector's direction
frame = vm.Frame3D(frame_center, dir1, dir2, dir3)
  • Create a plane where the face will be :
plane = vm.faces.Plane3D(frame) #from vm.faces we create a plane thanks to frame. Plane will be based on dir1 and dir2
  • Create a Surface2D :
out_contour = vm.wires.ClosedPolygon2D([vm.Point2D(-1, -1), vm.Point2D(1, -1), vm.Point2D(1, 1), vm.Point2D(-1, 1)])
surface2d = vm.faces.Surface2D(outer_contour=out_contour, inner_contours=[]) #Our contour doesn't have hole this is why inner_contours=[]
  • Create a face :
planeface = vm.faces.PlaneFace3D(surface3d=plane, surface2d=surface2d) #We put the surface2d on the plane and we have 3D surface
  • 3D display is available if your class contains a 'volmdlr_primitives' method in it. This method has to return volmdlr elements.
planeface.babylonjs() #if you want to display planeface created
By typing the following code, our constraint environment appears:
import tutorials.tutorial5_piping as tuto
import plot_data.core as plot_data
import volmdlr as vm
 
f1 = vm.Frame3D(vm.Point3D(0.05, 0.1, 0), vm.Vector3D(1, 0, 0), vm.Vector3D(0, 1, 0), vm.Vector3D(0, 0, 1))
p1 = vm.faces.Plane3D(f1)
s1 = vm.faces.Surface2D(outer_contour=vm.wires.ClosedPolygon2D([vm.Point2D(-0.05, -0.1),
vm.Point2D(0.05, -0.1),
vm.Point2D(0.05, 0.1),
vm.Point2D(-0.05, 0.1)]),
inner_contours=[])
face1 = vm.faces.OpenShell3D([vm.faces.PlaneFace3D(surface3d=p1, surface2d=s1)])
face1.color = (92/255, 124/255, 172/255)
face1.alpha = 1
 
f2 = vm.Frame3D(vm.Point3D(0.05, 0.1, 0.005), vm.Vector3D(1, 0, 0), vm.Vector3D(0, 0, 1), vm.Vector3D(0, 1, 0))
p2 = vm.faces.Plane3D(f2)
s2 = vm.faces.Surface2D(outer_contour=vm.wires.ClosedPolygon2D([vm.Point2D(-0.05, -0.005),
vm.Point2D(0.05, -0.005),
vm.Point2D(0.05, 0.005),
vm.Point2D(-0.05, 0.005)]),
inner_contours=[])
face2 = vm.faces.OpenShell3D([vm.faces.PlaneFace3D(surface3d=p2, surface2d=s2)])
face2.color = (92/255, 124/255, 172/255)
face2.alpha = 1
 
housing = tuto.Housing(faces=[face1, face2], origin=vm.Point3D(0, 0, 0))
housing.babylonjs()
 

housing

2.1.2 - Neutral fiber

Next step, we need to build the neutral fiber of our pipe. For that, we will create Frame class that will be those 'Checkpoint' for the pipe.

How to create a Frame

A Frame will characterize the path of our pipe. It needs a start and an end. In our case, there will be 4 Frames.

p_start = vm.Point3D(0, 0.1, 0.01) #x,y,z position
p_end = vm.Point3D(0.05, 0.1, 0.01)
frame1 = tuto.Frame(start = p_start_, end = p_end)

2.1.3 - Pipe

To finish the problem description, we need to instantiate a Piping class.

How to create a Piping

A Piping will be characterized by simple elements that will be presented below.

p_start = vm.Point3D(0, 0, 0) #x,y,z position, start
p_end = vm.Point3D(0.1, 0, 0) #end
 
dir_start = vm.Vector3D(0, 0, 1) #pipe start normal
dir_end = vm.Vector3D(1, 0, 1) #pipe end normal
 
pipe_diam = 0.005 #pipe diameter
connect_length = 0.1 #what distance do you want start and end to follow their normal to a potential connector.
minimum_radius = 0.03 #minimum radius of curvature allowed in the neutral fiber
 
piping1 = tuto.Piping(start=p_start, end=p_end,
                      direction_start=dir_start, direction_end=dir_end,
                      diameter=pipe_diam, length_connector=connect_length, minimum_radius=minimum_radius)

2.2 - Define engineering simulations needed

Once our basic classes created, we have to put them together and create a solution that will be optimized. For that, we implement an Assembly class.

How to create an Assembly

Thanks to elements created before, we can define an Assembly.

assembly1 = tuto.Assembly(frames=[frame1, frame3, frame4, frame2], piping=piping1, housing=housing)

This class will be used to find the optimum thanks to optimizer.

solution

2.3 - Create elementary generator & optimizer

To build our optimizer, we need to import some packages :

from random import random
import cma

In Optimize class, we would like to explain to you the aim of the Optimize method. At each line, there is a comment if needed.

  • Concerning 'cma.fmin', this function will find the best solution using 'self.objective' and an initialized vector solution. The method uses a vector that refers to position of point in order to respect condition concerning minimal radius.
    def optimize(self, assemblies: List[Assembly], number_solution_per_assembly:int)->List[Assembly]:
        solutions = []
        for assembly in assemblies: #we would find every best solutions concerning a list of assembly
            self.assembly = assembly
 
            x0a = [random() for i in range(len(self.assembly.frames))] #First vector to initialize solving
 
            check =True
            compt = 0
            number_solution = 0
            while check:
                xra, fx = cma.fmin(self.objective, x0a, 0.1,
                                   options={'bounds': [0, 1], #each point can be placed thanks to a percentage and the direction of each frame
                                            'tolfun': 1e-8, #precision of each solution returned by self.objective
                                            'verbose': 10,
                                            'ftarget': 1e-8, #if self.objective return is smaller than 'ftarget', a solution is found
                                            'maxiter': 10})[0:2] #maximum iteration wanted per searching, it chooses the best solution
                waypoints = self.assembly.waypoints
                radius = self.assembly.piping.genere_neutral_fiber(waypoints).radius #all radius characterizing neutral fiber
                min_radius = min(list(radius.values())) #we accept the solution with a 10% tolerance
                if min_radius >= 0.9*self.assembly.piping.minimum_radius and len(list(radius.keys())) == len(waypoints) - 2:
                    new_assembly = self.assembly.copy()
                    new_assembly.update(xra)
                    solutions.append(new_assembly)
                compt += 1
                number_solution += 1
                if compt == 20 or number_solution_per_assembly == number_solution:
                    break
 
        return solutions
 
    def objective(self, x):
        objective = 0
        self.update(x) #update assembly by a potential solution
 
        waypoints = self.assembly.waypoints
        radius = self.assembly.piping.genere_neutral_fiber(waypoints).radius
        min_radius = min(list(radius.values()))
        if min_radius < self.assembly.piping.minimum_radius:
            objective += 10 + (min_radius - self.assembly.piping.minimum_radius)**2 #penalization if a radius is smaller that expected
        else:
            objective += 10 - 0.1*(min_radius - self.assembly.piping.minimum_radius)
 
        return objective
 
    def update(self, x):
        self.assembly.update(x)

If you have more question concerning this part, we recommend you to read Simple power-transmission optimization.

2.4 - Build your workflow

First of all, in a workflow.py file, you have to create all assemblies you want to optimize. Each assembly will have something different, for example minimum radius allowed. Do not forget to import packages recommended :

import tutorials.tutorial5_piping as tuto
import volmdlr as vm
import dessia_common.workflow as wf

Once done, we will begin the conscrution of workflow.

  • Transform Optimizer object into workflow blocks.
import dessia_common.workflow as wf
 
block_optimizer = wf.InstanciateModel(tuto.Optimizer, name='Optimizer')
method_optimize = wf.MethodType(class_=tuto.Optimizer,  name='optimize')
block_optimize = wf.ModelMethod(method_type=method_optimize, name='optimize')
  • In our case, elements are already generated and given to the optimizer. Now, we want to display in the platform some elements. Once elements generated in platform, we could see them thanks to a ParallelPlot.
list_attribute1 = ['length', 'min_radius', 'max_radius', 'distance_input', 'straight_line']
display_reductor = wf.ParallelPlot(list_attribute1, 1, name='Display')
  • We have all what we need, let's create the workflow.
block_workflow = [block_optimizer, block_optimize, display_reductor] #task order in workflow
 
pipe_worflow = [wf.Pipe(block_optimizer.outputs[0], block_optimize.inputs[0]), #connection in workflow
                wf.Pipe(block_optimize.outputs[0], display_reductor.inputs[0])]
 
workflow = wf.Workflow(block_workflow, pipe_worflow, block_optimize.outputs[0]) #last element refers to solutions
 
input_values = {workflow.index(block_optimize.inputs[1]): assemblies,
                workflow.index(block_optimize.inputs[2]): 1,
                }

Workflow in your platform

To put your workflow in your platform, you have to type the following command after your workflow :

from dessia_api_client import Client
c = Client(api_url='https://api.YOURPLATFORM.dessia.tech')
r = c.create_object_from_python_object(workflow_run)