API Interface
Contents
API Interface#
Users have the flexibility to interact with Artisan using either Python or C++ through its API interface. This interface allows Artisan to be integrated as a computational engine backend or component for various application development needs. The Artisan API is designed to be compatible with keywords for simplicity and consistency. Users can access its functionalities through keyword-based inputs and retrieve the geometry (e.g., vertices and faces) at any design stage. The examples in this section can be found under the Interface
folder.
Python API#
Artisan has the native Python APIs. User may import the ArtisanMain and call the functions as with other python modulus. In general API interface provides three simple ways to access the Artisan computational power, Execution
, Run
and WorkItem
. Former two functions allows user direct inputs the entire JSON string through a string or a file, whereas the latter enable user to manipulate the lattice design step by step and extract the geometry at any design stage.
The Run
function reads the specified JSON file, imports the data, and performs the defined calculations. One needs to note that, the working directory is no longer the directory of ArtisanMain.py
and Artisan package. The application script working directory should be considered as the current directory, therefore the working path related definition in the input JSON file should be updated as well.
# Add the ArtisanMain.py path into your system path
import sys,os, pathlib
ArtisanRoot = pathlib.Path("..//Artsian_PY39//")
sys.path.append(ArtisanRoot)
# Simply import ArtisanModel. It is a function returns the API class
from ArtisanMain import Run
filename = ArtisanRoot / "Test_json//EngineBracket_HS_Infill_Lin_LR.txt"
Run(filename)
The Execution
function takes the input JSON string to perform the calculation. Additional file name are required in order to save log output information.
# Add the ArtisanMain.py path into your system path
import sys,os, pathlib
ArtisanRoot = pathlib.Path("..//Artsian_PY39//")
sys.path.append(ArtisanRoot)
# Simply import ArtisanModel. It is a function returns the API class
from ArtisanMain import Execution
import json
logfilename = ArtisanRoot / "Test_json//Test_log.txt"
cmd_dict = {"Setup":{ "Type" : "Geometry",
"Geomfile": ".//sample-obj//shell_1_of_bdd_.stl",
"Rot" : [0.0,0.0,0.0],
"res":[0.75,0.75,0.75],
"Padding": 1,
"onGPU": False,
"memorylimit": 16106127360
},
"WorkFlow":{
"1": {"Add_Lattice":{
"la_name": "Cubic", "size": [8.0,8.0,8.0], "thk":1.2, "Rot":[0.0, 0.0, 0.0], "Trans":[0.0, 0.0, 0.0],
"Inv": False, "Fill": True, "Cube_Request": {}
}
},
"2":{"Export": {"outfile": ".//Test_results/BingDunDun_Infill.stl"}}
},
"PostProcess":{"CombineMeshes": True,
"RemovePartitionMeshFile": False,
"RemoveIsolatedParts": True,
"ExportLazPts": False}
}
Execution(json.dumps(cmd_dict), logfilename)
Manipulating the design process through WorkItem
is as easy as writing the JSON string. The working flow keywords are feed sequentially through WorkItem
function, as demonstrated below. The order of the keywords are important as they build the lattice step by step.
# Add the ArtisanMain.py path into your system path
import sys,os
sys.path.append("..//Artsian_PY39//")
# Simply import ArtisanModel. It is a function returns the API class
from ArtisanMain import ArtisanModel
# Call ArtisanModel to buildup the API class
Model = ArtisanModel()
# Setup section in dict structure
cmd_setup = {
"Type" : "Geometry",
"Geomfile": "..//..//Src//sample-obj//shell_1_of_bdd_.stl",
"Rot" : [0.0,0.0,0.0],
"res":[0.75,0.75,0.75],
"Padding": 1,
"onGPU": False,
"memorylimit": 16106127360
}
# Push to the Setup method, and it setup the model building required infrastructure,
# it also calculates all fields for geometry.
Model.Setup(cmd_setup)
# A simple add lattice commands. The model always starts from Add_Lattice
cmd_workitem = {"Add_Lattice":{
"la_name": "Cubic", "size": [8.0,8.0,8.0], "thk":1.2, "Rot":[0.0, 0.0, 0.0], "Trans":[0.0, 0.0, 0.0],
"Inv": False, "Fill": True, "Cube_Request": {}
}
}
# Call the workitem method to calculates the defined workitem above.
Model.WorkItem(cmd_workitem)
# A repeat call of workitem to save the geometry in a stl file.
cmd_workitem = {"Export": {"outfile": "..//..//Src//Test_results/BingDunDun_Infill.stl"}}
Model.WorkItem(cmd_workitem)
# alternatively, user may extract the surfaces, including the vertices and triangulations.
verts, faces = Model.ExtractSurf()
C++ API#
The interaction between C++ and Artisan is just as straightforward as with Python. C++ applications can integrate the Artisan module through the embedded Python style. Users can refer to official documentation on the following topics:
Embedding Python in Another Application: https://docs.python.org/3.9/extending/embedding.html
Using NumPy C-API: https://numpy.org/doc/stable/user/c-info.html
Extending Python with C or C++: https://docs.python.org/3.9/extending/extending.html
User may also consider use the well-known third party package to further enhance the interaction between python and C++, such as pybind11 (https://pybind11.readthedocs.io/en/stable/advanced/embedding.html). This section will not cover this part, and will take official python interface as the coding base since it’s simply straightforward.
To compile successfully, users are required to link to the Python libraries. Since Artisan relies on NumPy array-based data operations, compilation requires NumPy library support. The cmake code below shows a simple way of integrating both libs for compilation.
cmake_minimum_required(VERSION 3.0.0)
project(CheckArtisan VERSION 0.1.0)
include(CTest)
enable_testing()
add_executable(Artisan ./src/Artisan_Example_01.cpp)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
# find_package(PythonLibs REQUIRED)
include_directories(C:/ProgramData/Anaconda3/include) # Python include folder
include_directories(C:/ProgramData/Anaconda3/libs) # Python link lib
include_directories(C:/ProgramData/Anaconda3/Lib/site-packages/numpy/core/include) # Numpy include folder
target_include_directories(Artisan PUBLIC C:/ProgramData/Anaconda3/include)
target_link_directories(Artisan PUBLIC C:/ProgramData/Anaconda3/libs)
target_link_libraries(Artisan PUBLIC C:/ProgramData/Anaconda3/libs/python39.lib)
Similar to the usage in Python, Artisan offers three different styles of API interfaces. Users can launch the design computation directly by reading the JSON file or through a JSON stream. To do so, they can call the Run
function and pass in the JSON filename as input to launch the computation. Alternatively, users can push a string data that contains the JSON structure to the Execution
function to initiate the computation. And user may manipulate the design process through WorkItem
function.
The sample code below demonstrates the basic usage of Run
function. The code loads the modulus using PyImport_Import
function, and execute the function using PyObject_CallObject
. Note that the function returns NULL
.
#include "Python.h"
#include <iostream>
#include <fstream>
#include <conio.h>
int main(int argc, char *argv[])
{
// References:
// Embedding Python in Another Application
// https://docs.python.org/3.9/extending/embedding.html
// The code below demonstrates how to call the "Run" function which takes one argument - filename
// It reads the file containing the json, and then execute the file and construct the model.
// commands: Artisan.exe <-- the json file path here -->
// Examples: Artisan.exe .//Test_json//crank_handle_infill.txt
// Note this is only an example since we take the first argument after Artisan.exe as input.
PyObject *pName, *pModule, *pDict, *pFunc, *pArgs, *pValue;
// Initialize the Python Interpreter
Py_Initialize();
// Imports ArtisanMain.py
char filename[] = "ArtisanMain";
pName = PyUnicode_FromString(filename);
// Load the module object
pModule = PyImport_Import(pName);
// Check whether the module was loaded.
if (pModule != NULL) {
std::cout<< "Modulue loaded \n";
}else{
std::cout<< "Fail: Modulue loaded \n";
}
// Access to the function "Run" in "ArtisanMain.py".
// Run(filename)
// This function will read the "filename" - a JSON file and execute the file.
pFunc = PyObject_GetAttrString(pModule, "Run");
if (pFunc && PyCallable_Check(pFunc)) {
std::cout<< "Function loaded \n";
}else{
std::cout<< "Fail: Function loaded \n";
}
// Define the function parameter values, as a python tuple
pArgs = PyTuple_New(1); // we have one parameter here, the JSON file name.
PyTuple_SetItem(pArgs, 0, PyUnicode_FromString(argv[1]));
// Call function
pValue = PyObject_CallObject(pFunc, pArgs);
// Clean up
Py_DECREF(pModule);
Py_DECREF(pFunc);
Py_DECREF(pName);
Py_DECREF(pArgs);
// Finish the python interpreter
Py_Finalize();
return 0;
}
User may choose passing the c-style JSON string to Artisan, rather than read from external files. The code below demonstrate how to interact with the Execution
function in ArtisanMain
. This function requires additional argument that indicates the file name for log output.
#include "Python.h"
#include <iostream>
#include <fstream>
#include <conio.h>
int main(int argc, char *argv[])
{
// This demo shows how to pass a c-style string with JSON to the execution function
// User are required to give a logfile name as well.
PyObject *pName, *pModule, *pDict, *pFunc, *pArgs, *pValue;
char TestString[] = "{\"Setup\": {\"Type\": \"Geometry\", \
\"Geomfile\": \".//sample-obj//shell_1_of_bdd_.stl\", \
\"Rot\": [0.0, 0.0, 0.0], \
\"res\": [0.75, 0.75, 0.75], \
\"Padding\": 1, \"onGPU\": false, \
\"memorylimit\": 16106127360 \
}, \
\"WorkFlow\": { \
\"1\": { \
\"Add_Lattice\": {\"la_name\": \"Cubic\", \
\"size\": [8.0, 8.0, 8.0], \
\"thk\": 1.2, \
\"Rot\":[0.0, 0.0, 0.0], \
\"Trans\":[0.0, 0.0, 0.0], \
\"Inv\": false, \
\"Fill\": true, \
\"Cube_Request\": {}}}, \
\"2\": { \
\"Export\": {\"outfile\": \".//Test_results/BingDunDun_Infill.stl\"}}}, \
\"PostProcess\": { \
\"CombineMeshes\": true, \
\"RemovePartitionMeshFile\": false, \
\"RemoveIsolatedParts\": true, \
\"ExportLazPts\": false}}" ; \
char filename[] = "ArtisanMain";
char logfile[] = "Testlogfile.log"; // logfile file name
Py_Initialize();
pName = PyUnicode_FromString(filename);
// Load the module object
pModule = PyImport_Import(pName);
// Check whether the module was loaded.
if (pModule != NULL) {
std::cout<< "Modulue loaded \n";
}else{
std::cout<< "Fail: Modulue loaded \n";
}
// Access to the function "Run" in "ArtisanMain.py".
// Run(filename)
// This function will read the "filename" - a JSON file and execute the file.
pFunc = PyObject_GetAttrString(pModule, "Execution");
if (pFunc && PyCallable_Check(pFunc)) {
std::cout<< "Function loaded \n";
}else{
std::cout<< "Fail: Function loaded \n";
}
// Define the function parameter values, as a python tuple
pArgs = PyTuple_New(2); // we have two parameter here, one json like string, and logfile filename.
PyTuple_SetItem(pArgs, 0, PyUnicode_FromString(TestString));
PyTuple_SetItem(pArgs, 1, PyUnicode_FromString(logfile));
// Call function
pValue = PyObject_CallObject(pFunc, pArgs);
// Clean up
Py_DECREF(pModule);
Py_DECREF(pFunc);
Py_DECREF(pName);
Py_DECREF(pArgs);
// Finish the python interpreter
Py_Finalize();
return 0;
}
Users can use the WorkItem
method of the API object to perform various design and export operations on the lattice field. By passing the appropriate workflow keywords as inputs to WorkItem
, users can apply specific operations to manipulate the lattice field. Once the workflow has been completed, users can extract the resultant geometry using the ExtractSurf
method. This provides flexibility to users in terms of defining their own customized workflows for specific applications and use cases. The extracted geometry can be further processed or exported as per the user’s requirement. The example below showed the usage of these methods.
#include "Python.h"
#include <numpy/arrayobject.h>
#include <iostream>
#include <cstdint>
int main(int argc, char *argv[])
{
// This example demonstrates how to access Artisan APIs through Setup, WorkItem and ExtractSurf
// Note that the codes is the embedded python interpreter into the application.
// Coding References:
// Extending Python with C or C++
// https://docs.python.org/3.9/extending/extending.html
// Embedding Python in Another Application
// https://docs.python.org/3.9/extending/embedding.html
// Using NumPy C-API
// https://numpy.org/doc/stable/user/c-info.html
PyObject *pName, *pModule, *pDict, *pFunc, *pArgs, *pValue;
PyObject *ArtisanModel, *AM_Setup, *AM_WorkItem, *AM_ExtractSurf;
PyObject *pFunc_ArgsVal;
char filename[] = "ArtisanMain";
Py_Initialize();
pName = PyUnicode_FromString(filename);
// Load the module object
pModule = PyImport_Import(pName);
// Check whether the module was loaded.
if (pModule != NULL) {
std::cout<< "Artian Modulue loaded \n";
}else{
std::cout<< "Fail: Artisan Modulue loaded \n";
}
// Access to the function "ArtisanModel" in "ArtisanMain.py".
// ArtisanModel() has to be called to return the API object.
ArtisanModel = PyObject_CallObject(PyObject_GetAttrString(pModule, "ArtisanModel"),
Py_BuildValue("()"));
// Check whether the API object loaded or not
if (ArtisanModel != NULL) {
std::cout<< "API object loaded \n";
}else{
std::cout<< "Fail: API object loaded \n";
}
// Access the setup method
AM_Setup = PyObject_GetAttrString(ArtisanModel, "Setup");
if (PyCallable_Check(AM_Setup)) {
std::cout<< "AM_Setup Function loaded \n";
}else{
std::cout<< "Fail: AM_Setup Function loaded \n";
}
// Build up the input Dict data object for setup.
pFunc_ArgsVal = Py_BuildValue("{s:s, s:s, s:O, s:O, s:i, s:O, s:l}",
"Type", "Geometry",
"Geomfile", ".//sample-obj//shell_1_of_bdd_.stl",
"Rot", Py_BuildValue("[d, d, d]", 0.0, 0.0, 0.0),
"res", Py_BuildValue("[d, d, d]", 0.75, 0.75, 0.75),
"Padding", 1,
"onGPU", Py_False,
"memorylimit", 16106127360);
// Define the function parameter values, as a python tuple
pArgs = PyTuple_New(1); // we have one parameter here - a Dict data object.
PyTuple_SetItem(pArgs, 0, pFunc_ArgsVal);
// Call Setup method
PyObject_CallObject(AM_Setup, pArgs);
// Build up the input Dict data object of the 1st WorkItem.
// Add a Cubic Lattice
pFunc_ArgsVal = Py_BuildValue("{s:s, s:O, s:d, s:O, s:O, s:O, s:O, s:O}",
"la_name", "Cubic",
"size", Py_BuildValue("[d, d, d]", 8.0, 8.0, 8.0),
"thk", 1.2,
"Rot", Py_BuildValue("[d, d, d]", 0.0, 0.0, 0.0),
"Trans", Py_BuildValue("[d, d, d]", 0.0, 0.0, 0.0),
"Inv", Py_False,
"Fill", Py_True,
"Cube_Request", Py_BuildValue("{}"));
pArgs = PyTuple_New(1); // we have one parameter here - a Dict data object.
PyTuple_SetItem(pArgs, 0, Py_BuildValue("{s:O}", "Add_Lattice", pFunc_ArgsVal));
// Get the WorkItem function
AM_WorkItem = PyObject_GetAttrString(ArtisanModel, "WorkItem");
if (PyCallable_Check(AM_WorkItem)) {
std::cout<< "AM_WorkItem Function loaded \n";
}else{
std::cout<< "Fail: AM_WorkItem Function loaded \n";
}
// Call WorkItem - Add_Lattice
PyObject_CallObject(AM_WorkItem, pArgs);
// Build up the input Dict data object of the 2nd WorkItem.
// Export the surfaces, save them as stl file.
pFunc_ArgsVal = Py_BuildValue("{s:O}", "Export",
Py_BuildValue("{s:s}",
"outfile", ".//Test_results/BingDunDun_Infill.stl"));
PyTuple_SetItem(pArgs, 0, pFunc_ArgsVal);
// Call WorkItem - Export
PyObject_CallObject(AM_WorkItem, pArgs);
// Alternatively we could get the vertices and faces here
AM_ExtractSurf= PyObject_GetAttrString(ArtisanModel, "ExtractSurf");
// Call ExtractSurf method to extract the surfaces of the current design.
PyObject* pGeoms = PyObject_CallObject(AM_ExtractSurf, Py_BuildValue("()"));
// Extract the returned numpy arrays
// Vertices
PyArrayObject* verts = reinterpret_cast<PyArrayObject*>(PyTuple_GetItem(pGeoms, 0));
// Surfaces
PyArrayObject* faces = reinterpret_cast<PyArrayObject*>(PyTuple_GetItem(pGeoms, 1));
// Convert the numpy arrays to C++ arrays
// This is a critical step that the data types have to match python returned values.
// Vertices, float numbers.
float* verts_c = static_cast<float*>(PyArray_DATA(verts));
// Faces, unsigned int32
uint32_t* faces_c = static_cast<uint32_t*>(PyArray_DATA(faces));
// Get the shape of arrays
npy_intp* verts_shape = PyArray_SHAPE(verts);
npy_intp* faces_shape = PyArray_SHAPE(faces);
std::cout << "Vertices Matrix Shape:" << " ";
std::cout << verts_shape[0] << " "; // Number of the vertices
std::cout << verts_shape[1] << " "; // Coordinate dimension, i.e. 3, in 3D space
std::cout << std::endl;
std::cout << "Faces Matrix Shape:" << " ";
std::cout << faces_shape[0] << " "; // Number of the faces
std::cout << faces_shape[1] << " "; // Each face contains three vertices
std::cout << std::endl;
// Print out the last vertices coordinate
std::cout << "Coordinate of the last vertices:" << " ";
std::cout << verts_c[(verts_shape[0]-1) * verts_shape[1] + 0] << " ";
std::cout << verts_c[(verts_shape[0]-1) * verts_shape[1] + 1] << " ";
std::cout << verts_c[(verts_shape[0]-1) * verts_shape[1] + 2] << " ";
std::cout << std::endl;
// Print out the last surfaces vertices index
std::cout << "Vertices index of the last surface:" << " ";
std::cout << faces_c[(faces_shape[0]-1) * faces_shape[1] + 0] << " ";
std::cout << faces_c[(faces_shape[0]-1) * faces_shape[1] + 1] << " ";
std::cout << faces_c[(faces_shape[0]-1) * faces_shape[1] + 2] << " ";
std::cout << std::endl;
// Print the data in the numpy arrays
// for (npy_intp i = 0; i < verts_shape[0]; i++) {
// for (npy_intp j = 0; j < verts_shape[1]; j++) {
// std::cout << verts_c[i * verts_shape[1] + j] << " ";
// }
// std::cout << std::endl;
// }
// for (npy_intp i = 0; i < faces_shape[0]; i++) {
// for (npy_intp j = 0; j < faces_shape[1]; j++) {
// std::cout << faces_c[i * faces_shape[1] + j] << " ";
// }
// std::cout << std::endl;
// }
// Use below to print out the error message (if any).
PyErr_Print();
// Clean up
Py_DECREF(pModule);
Py_DECREF(pFunc);
Py_DECREF(pName);
Py_DECREF(pArgs);
Py_DECREF(pGeoms);
Py_DECREF(pFunc_ArgsVal);
Py_DECREF(ArtisanModel);
Py_DECREF(AM_Setup);
Py_DECREF(AM_WorkItem);
Py_DECREF(AM_ExtractSurf);
Py_DECREF(verts);
Py_DECREF(faces);
// Finish the python interpreter
Py_Finalize();
return 0;
}