Dave Loffredo
2023-02-08

JSON From STEP-NC

We have been investigating JSON for describing manufacturing transactions and put together a small program to generate some using the STEP Python interface. We felt this was a nice example, so we are sharing it below.

The JSON output for the simple facing workingstep is shown below. It summarizes the position within the process, tool, operation, feature, and some other basic parameters.

2023-02-09 - Updated with some toolpath information

Simple facing workingstep

Simple Facing Workingstep
{
    name: thready_facing_op_revolved,
    tools: {
        1: {
            id: 1,
            type: ENDMILL,
            overall_assembly_length: {
                value: 1.0,
                unit: MM
            },
            effective_cutting_diameter: {
                value: 50.6,
                unit: MM
            },
            maximum_depth_of_cut: {
                value: 1.5,
                unit: MM
            },
            hand_of_cut: null,
            coolant_through_tool: null,
            number_of_effective_teeth: 1.0,
            edge_radius: {
                value: 0.03,
                unit: MM
            },
            tool_cutting_edge_angle: {
                value: 0.0,
                unit: DEG
            }
        }
    },
    project: {
        id: thready_facing_op_revolved,
        main_workplan: {
            id: Workplan,
            type: WORKPLAN,
            enabled: true,
            as_is_geometry: null,
            to_be_geometry: e2408c22-a893-11ed-a9b5-18dbf245276c,
            removal_geometry: null,
            twin_start: 2023-02-07T16:32:31-05:00,
            twin_end: 2023-02-07T16:32:32-05:00,
            elements: [
                {
                    id: minimal geometry facing,
                    type: WORKPLAN,
                    enabled: true,
                    as_is_geometry: null,
                    to_be_geometry: null,
                    removal_geometry: null,
                    twin_start: 2023-02-07T16:32:31-05:00,
                    twin_end: 2023-02-07T16:32:32-05:00,
                    elements: [
                        {
                            id: WS 1 face roughing and finishing,
                            type: MACHINING_WORKINGSTEP,
                            enabled: true,
                            as_is_geometry: null,
                            to_be_geometry: null,
                            removal_geometry: null,
                            twin_start: 2023-02-07T16:32:31-05:00,
                            twin_end: 2023-02-07T16:32:32-05:00,
                            operation: {
                                id: ,
                                type: PLANE_FINISH_MILLING,
                                tool: {
                                    $ref: #/tools/1
                                },
                                toolpath: [
                                    {
                                        id: clear WS 1 TP 1,
                                        type: CUTTER_LOCATION_TRAJECTORY,
                                        basiccurve: {
                                            name: ,
                                            type: polyline,
                                            points: [
                                                [
                                                    0.0,
                                                    -53.36,
                                                    115.0
                                                ],
                                                [
                                                    0.0,
                                                    -53.36,
                                                    110.0
                                                ]
                                            ]
                                        }
                                    },
                                    {
                                        id: entry WS 1 TP 2,
                                        type: CUTTER_LOCATION_TRAJECTORY,
                                        basiccurve: {
                                            name: ,
                                            type: polyline,
                                            points: [
                                                [
                                                    0.0,
                                                    -53.36,
                                                    110.0
                                                ],
                                                [
                                                    0.0,
                                                    -53.36,
                                                    104.0
                                                ]
                                            ]
                                        }
                                    },
                                    {
                                        id: WS 1 TP 3,
                                        type: CUTTER_LOCATION_TRAJECTORY,
                                        basiccurve: {
                                            name: ,
                                            type: polyline,
                                            points: [
                                                [
                                                    0.0,
                                                    -53.36,
                                                    104.0
                                                ],
                                                [
                                                    0.0,
                                                    53.36,
                                                    104.0
                                                ]
                                            ]
                                        }
                                    },
                                    {
                                        id: WS 1 TP 4,
                                        type: CUTTER_LOCATION_TRAJECTORY,
                                        basiccurve: {
                                            name: ,
                                            type: polyline,
                                            points: [
                                                [
                                                    0.0,
                                                    53.36,
                                                    104.0
                                                ],
                                                [
                                                    0.0,
                                                    53.36,
                                                    115.0
                                                ]
                                            ]
                                        }
                                    }
                                ],
                                axial_cutting_depth: {
                                    value: 1.0,
                                    unit: MM
                                },
                                overcut_length: null
                            },
                            feature: {
                                id: disk feature,
                                type: REVOLVED_FLAT,
                                workpiece: 6b35d480-930c-451e-93c6-232565a10e05,
                                explicit_representation: [
                                    69ebf3b7-4982-42f1-80c6-51dde4459c0d
                                ],
                                placement: [
                                    1.0,
                                    0.0,
                                    0.0,
                                    0.0,
                                    0.0,
                                    1.0,
                                    0.0,
                                    0.0,
                                    0.0,
                                    0.0,
                                    1.0,
                                    0.0,
                                    0.0,
                                    0.0,
                                    104.0,
                                    1.0
                                ],
                                profile_length: {
                                    value: 46.0,
                                    unit: MM
                                }
                            }
                        }
                    ],
                    setup: null
                }
            ],
            setup: null
        }
    }
}

The program to make the JSON is shown below. Just using the built in python json.dumps on a STEP object gives an "Object is not JSON serializable" error. Beating on the jsons package gets a bit further but ultimately runs into a circular reference error.

In any case, just serializing AIM and ARM attributes in a mechanical manner would not be useful to the receiver. We define a JSONEncoder subclass to handle the different ARM constructs and fine tune the JSON into something useful for a particular purpose.

The JSONEncoder class uses the JSONFMT table of functions keyed on the ARM type to find a function that returns a plain dictionary for a given STEP construct, which is used for the json. Customization is done with a special function for each ARM type.

# MAKE JSON FROM STEP OBJECTS
#

import sys
import json
import uuid

from json import JSONEncoder

from pathlib import Path
from steptools import step
from steptools.step import AptAPI as apt
import stepdatetime

# JSON utility functions -----------------------------------------------

def json_boolean(strval, tval, fval, dflt=None) -> bool:
    if not strval: return dflt
    strval = strval.casefold()
    if strval == tval.casefold(): return True
    if strval == fval.casefold(): return False
    return None

def exec_enabled(o) -> bool:
    # convert strings and defaults to simple T/F
    if o is None: return False
    return json_boolean(o['enabled'], 'enabled', 'disabled', True)

def json_uuid(o) -> str:
    if o is None: return None

    # get existing or set to new uuid
    U = apt.set_uuid(o, str(uuid.uuid1()))
    return U

def json_faces(lst) -> list:
    if len(lst) == 0: return None
    VAL = list()
    for o in lst:
        VAL.append(json_uuid(o))
    return VAL
    
def json_measure(o) -> dict:
    if o is None: return None
    if step.isinstance(o, 'measure_with_unit'):
        V = o.value_component[0]
        U = step.Unit.fromstep(o.unit_component).name
        return { 'value': V, 'unit': U }

    return None

def json_tool_ref(o) -> dict:
    if o is None: return None
    return {
        '$ref': '#/tools/' + o['its_id'],
    }


# EXECUTABLES --------------------------------------------------

# ARM atts on executables
# 'as_is_geometry': None,
# 'enabled': None,
# 'fixture_geometry': None,
# 'its_id': 'Workingstep 1',
# 'its_security_classification': <step.ArmCollection size 0>,
# 'machine_used': None,
# 'process_properties': <step.ArmCollection size 0>,
# 'removal_geometry': None,
# 'to_be_geometry': None,
# 'twin_end': None,
# 'twin_exception': None,
# 'twin_plan': None,
# 'twin_source': None,
# 'twin_start': None,
# 'twin_worktime': None}
# AIM atts on executables
# 'name': 'Workingstep 1',
# 'consequence': '',
# 'description': 'machining',
# 'purpose': '',
def json_exec(o) -> dict:
    D = {
        'id': o['its_id'],
        # 'uuid': json_uuid(o),
        'type': step.arm_type(o),
        'enabled': exec_enabled(o),
        'as_is_geometry': json_uuid(o['as_is_geometry']),
        'to_be_geometry': json_uuid(o['to_be_geometry']),
        'removal_geometry': json_uuid(o['removal_geometry']),
    }
    D['twin_start'] = stepdatetime.asisoformat(o.twin_start)
    D['twin_end'] = stepdatetime.asisoformat(o.twin_end)
    
    return D

def json_enabled_elements(lst) -> list:
    VAL = list()
    for o in lst:
        if exec_enabled(o):
            VAL.append(o)
    return VAL
            
# OPERATIONS --------------------------------------------------

def json_operation(o) -> dict:
    # 'approach': None,
    # 'cam_properties': <step.ArmCollection size 0>,
    # 'consequence': '',
    # 'description': '',
    # 'its_id': 'start',
    # 'its_machine_functions': <step.Object ARM MILLING_MACHINE_FUNCTIONS #39952 machining_functions>,
    # 'its_machining_strategy': None,
    # 'its_op_security_classification': <step.ArmCollection size 0>,
    # 'its_technology': <step.Object ARM MILLING_TECHNOLOGY #39931 machining_technology>,
    # 'its_tool': <step.Object ARM TWIST_DRILL #39905 machining_tool>,
    # 'its_tool_direction': None,
    # 'its_toolpath': <step.ArmCollection size 6>,
    # 'name': 'start',
    # 'overcut_length': None,
    # 'process_properties': <step.ArmCollection size 0>,
    # 'purpose': '',
    # 'retract': None,
    # 'retract_plane': None,
    # 'start_point': None    
    return {
        'id': o['its_id'],
        'type': step.arm_type(o),
        # not on probing
        # 'machine_functions': o['its_machine_functions'],
        # 'technology': o['its_technology'],
        'tool': json_tool_ref(o['its_tool']),
        'toolpath': o['its_toolpath'],
    }


def json_drilling(o) -> dict:
    D = json_operation(o)
    D['cutting_depth'] = json_measure(o.cutting_depth)
    #D['previous_diameter'] = json_measure(o.previous_diameter)
    #D['dwell_time_bottom'] = json_measure(o.dwell_time_bottom)
    #D['feed_on_retract'] = json_measure(o.feed_on_retract)
    return D

def json_plane_milling(o) -> dict:
    D = json_operation(o)
    D['axial_cutting_depth'] = json_measure(o.axial_cutting_depth)
    D['overcut_length'] = json_measure(o.overcut_length)
    return D


# CUTTING TOOLS --------------------------------------------------
def json_tool(o) -> dict:
    return {
        'id': o['its_id'],
        'type': step.arm_type(o),
    }

def json_milling_machine_cutting_tool(o) -> dict:
    D = json_tool(o)
    # cutting components
    D['overall_assembly_length'] = json_measure(o.overall_assembly_length)
    D['effective_cutting_diameter'] = json_measure(o.effective_cutting_diameter)
    D['maximum_depth_of_cut'] = json_measure(o.maximum_depth_of_cut)
    D['hand_of_cut'] = o.hand_of_cut
    D['coolant_through_tool'] = json_boolean(
        o.hand_of_cut, 'supported', 'not supported'
    )
    return D

def json_milling_tool(o) -> dict:
    D = json_milling_machine_cutting_tool(o)
    D['number_of_effective_teeth'] = o.number_of_effective_teeth
    D['edge_radius'] = json_measure(o.edge_radius)
    return D

def json_endmill(o) -> dict:
    D = json_milling_tool(o)
    D['tool_cutting_edge_angle'] = json_measure(o.tool_cutting_edge_angle)
    return D
    
def json_drilling_tool(o) -> dict:
    D = json_milling_machine_cutting_tool(o)
    D['point_angle'] = json_measure(o.point_angle)
    return D

def json_countersink(o) -> dict:
    D = json_drilling_tool(o)
    D['minimum_cutting_diameter'] = json_measure(o.minimum_cutting_diameter)
    D['maximum_usable_length'] = json_measure(o.maximum_usable_length)
    return D


def json_user_defined_milling_tool(o) -> dict:
    D = json_milling_machine_cutting_tool(o)
    D['identifier'] = o.identifier
    D['corner_radius'] = json_measure(o.corner_radius)
    D['corner_radius_center_horizontal'] = json_measure(o.corner_radius_center_horizontal)
    D['corner_radius_center_vertical'] = json_measure(o.corner_radius_center_vertical)    
    D['taper_angle'] = json_measure(o.taper_angle)    
    D['tip_outer_angle'] = json_measure(o.tip_outer_angle)
    return D



# FEATURES --------------------------------------------------
def json_feature(o) -> dict:
    D = {
        'id': o['its_id'],
        'type': step.arm_type(o),
        'workpiece': json_uuid(o['its_workpiece']),
        'explicit_representation': json_faces(o['explicit_representation']),
    }
    try:
        D['placement'] = step.Xform.fromstep(o.feature_placement)
    except:
        pass
    return D

def json_revolved_flat(o) -> dict:
    D = json_feature(o)
    # flat edge shape has linear profile length
    PROF = o['flat_edge_shape']
    D['profile_length'] = json_measure(PROF.profile_length)
    return D
    

# TOOLPATHS --------------------------------------------------
def json_toolpath(o) -> dict:
    D = {
        'id': o['its_id'],
        'type': step.arm_type(o),
        'basiccurve': json_curve(o['basiccurve']),
    }
    return D


def json_curve(o) -> dict:
    if step.isinstance(o, 'polyline'):
        D = {
            'name': o['name'],
            'type': step.type(o),
            'points': [],
        }
        pts = D['points']
        for obj in o.points:
            pts.append(step.Vec.fromstep(obj))
        return D
    
    return o
    
# ------------------------------------------------------------
# FORMAT TABLE
#
# This is a simple dictionary that goes from the ARM type of an object
# to a function that returns a dictionary for the json.  To deal with
# the inheritance, I have some common functions for execs, operations,
# etc. that can be combined with | with a dictionary for the specifics
# of a particular ARM type.
# 
#
JSONFMT = {
    # 'description': None,
    # 'formation': <step.Object #11 product_definition_formation>,
    # 'frame_of_reference': <step.Object #17 product_definition_context>,
    # 'id': '',
    # 'its_id': 'thready_fixed',
    # 'its_manufacturer': None,
    # 'its_manufacturer_organization': None,
    # 'its_owner': <step.Object ARM PERSON_AND_ADDRESS #1190 person>,
    # 'its_owner_organization': None,
    # 'its_release': <step.Object #443 date_and_time>,
    # 'its_security_classification': <step.ArmCollection size 0>,
    # 'its_status': <step.Object ARM APPROVAL #412 approval>,
    # 'its_workpieces': <step.ArmCollection size 1>,
    # 'main_workplan': <step.Object ARM WORKPLAN #1469 machining_workplan>}
    'PROJECT': (lambda o: {
        'id': o['its_id'],
        'main_workplan': o['main_workplan'],
    }),

    # 'its_channel': None,
    # 'its_elements': <step.ArmCollection size 1>,
    # 'its_minimum_machine_params': None,
    # 'its_setup': <step.Object ARM SETUP #409 product_def inition_formation>,
    # 'planning_operation': None,
    # 'toolpath_orientation': <step.Object #1473 axis2_placement_3d>,
    'WORKPLAN': (lambda o: json_exec(o) | {
        'elements': json_enabled_elements(o['its_elements']),
        #'toolpath_orientation': step.Xform.fromstep(o['toolpath_orientation']),
        'setup': o['its_setup'],
    }),

    'SELECTIVE': (lambda o: json_exec(o) | {
        'elements': json_enabled_elements(o['its_elements']),
    }),
    'PARALLEL': (lambda o: json_exec(o) | {
        'branches': json_enabled_elements(o['branches']),
    }),
    'NON_SEQUENTIAL': (lambda o: json_exec(o) | {
        'elements': json_enabled_elements(o['its_elements']),
    }),

    
    # 'final_features': <step.ArmCollection size 0>,
    # 'its_feature': <step.Object ARM FEATURE_TEMPLATE #1485 datum_feature_and_instanced_feature_and_thread>,
    # 'its_operation': <step.Object ARM THREADING_ROUGH #1381 threading_turning_operation>,
    # 'its_secplane': <step.Object #379 plane>,
    # 'its_secplane_rep': <step.Object #378 representation>,
    # 'toolpath_orientation': None,

    'MACHINING_WORKINGSTEP': (lambda o: json_exec(o) | {
        'operation': o['its_operation'],
        'feature': o['its_feature'],
        #'toolpath_orientation': step.Xform.fromstep(o['toolpath_orientation']),
    }),

    # Operations
    'BORING': json_operation,
    'BOTTOM_AND_SIDE_FINISH_MILLING': json_operation,
    'BOTTOM_AND_SIDE_ROUGH_MILLING': json_operation,
    'CENTER_MILLING': json_operation,
    'CONTOURING_FINISH': json_operation,
    'CONTOURING_ROUGH': json_operation,
    'CUTTING_IN': json_operation,
    'DRILLING': json_drilling,
    'FACING_FINISH': json_operation,
    'FACING_ROUGH': json_operation,
    'FREEFORM_FINISH_MILLING': json_operation,
    'FREEFORM_OPERATION': json_operation,
    'FREEFORM_ROUGH_MILLING': json_operation,
    'GROOVING_FINISH': json_operation,
    'GROOVING_ROUGH': json_operation,
    'KNURLING': json_operation,
    'MULTISTEP_DRILLING': json_drilling,
    'PLANE_FINISH_MILLING': json_plane_milling,
    'PLANE_ROUGH_MILLING': json_plane_milling,
    'REAMING': json_operation,
    'SIDE_FINISH_MILLING': json_operation,
    'SIDE_ROUGH_MILLING': json_operation,
    'TAPPING': json_operation,
    'THREADING_FINISH': json_operation,
    'THREADING_ROUGH': json_operation,

    # Probing Operations
    'WORKPIECE_COMPLETE_PROBING': json_operation,
    'WORKPIECE_PROBING': json_operation,

    # NC Functions
    'DISPLAY_MESSAGE': json_exec,
    'EXTENDED_NC_FUNCTION': json_exec,
    'INDEX_TABLE': json_exec,
    'LOAD_TOOL': json_exec,
    'NON_SEQUENTIAL': json_exec,
    'OPTIONAL_STOP': json_exec,
    'PROGRAM_STOP': json_exec,
    'RETURN_HOME': json_exec,
    'UNLOAD_TOOL': json_exec,

    # Features
    'CATALOGUE_THREAD': json_feature,
    'CHAMFER': json_feature,
    'CIRCULAR_BOSS': json_feature,
    'CIRCULAR_CLOSED_SHAPE_PROFILE': json_feature,
    'CIRCULAR_PATTERN': json_feature,
    'CLOSED_POCKET': json_feature,
    'COMPLEX_BOSS': json_feature,
    'COMPOUND_FEATURE': json_feature,
    'COUNTERBORE_HOLE': json_feature,
    'COUNTERBORE_HOLE_TEMPLATE': json_feature,
    'COUNTERSUNK_HOLE': json_feature,
    'COUNTERSUNK_HOLE_TEMPLATE': json_feature,
    'DEF INED_MARKING': json_feature,
    'DEF INED_THREAD': json_feature,
    'DIAGONAL_KNURL': json_feature,
    'DIAMOND_KNURL': json_feature,
    'EDGE_ROUND': json_feature,
    'FEATURE_TEMPLATE': json_feature,
    'GENERAL_FEATURE': json_feature,
    'GENERAL_OUTSIDE_PROFILE': json_feature,
    'GENERAL_PATTERN': json_feature,
    'GENERAL_REVOLUTION': json_feature,
    'GENERAL_SHAPE_PROFILE': json_feature,
    'GROOVE': json_feature,
    'OPEN_POCKET': json_feature,
    'OUTER_DIAMETER': json_feature,
    'OUTER_DIAMETER_TO_SHOULDER': json_feature,
    'PARTIAL_CIRCULAR_SHAPE_PROFILE': json_feature,
    'PLACED_FEATURE': json_feature,
    'PLANAR_FACE': json_feature,
    'RECTANGULAR_BOSS': json_feature,
    'RECTANGULAR_CLOSED_SHAPE_PROFILE': json_feature,
    'RECTANGULAR_OPEN_SHAPE_PROFILE': json_feature,
    'RECTANGULAR_PATTERN': json_feature,
    'REVOLVED_FLAT': json_revolved_flat,
    'ROUND_HOLE': json_feature,
    'ROUND_HOLE_TEMPLATE': json_feature,
    'ROUNDED_END': json_feature,
    'SLOT': json_feature,
    'SPHERICAL_CAP': json_feature,
    'STEP': json_feature,
    'STRAIGHT_KNURL': json_feature,
    'TOOL_KNURL': json_feature,
    'TOOLPATH_FEATURE': json_feature,

    # Cutting Tools
    'BALLNOSE_ENDMILL': json_endmill,
    'BULLNOSE_ENDMILL': json_endmill,
    'COMBINED_DRILL_AND_REAMER': json_tool,
    'COMBINED_DRILL_AND_TAP': json_tool,
    'COUNTERBORE': json_drilling_tool,
    'COUNTERSINK': json_countersink,
    'DOVETAIL_MILL': json_milling_tool,
    'DRILL': json_drilling_tool,
    'ENDMILL': json_endmill,
    'FACEMILL': json_milling_tool,
    'GENERAL_TURNING_TOOL': json_tool,
    'GROOVING_TOOL': json_tool,
    'KNURLING_TOOL': json_tool,
    'PROFILED_END_MILL': json_endmill,
    'REAMING_CUTTING_TOOL': json_tool,
    'ROTATING_BORING_CUTTING_TOOL': json_tool,
    'SHOULDERMILL': json_milling_tool,
    'SIDE_MILL': json_milling_tool,
    'SPADE_DRILL': json_drilling_tool,
    'SPOTDRILL': json_drilling_tool,
    'STEP_DRILL': json_drilling_tool, # has more
    'T_SLOT_MILL': json_milling_tool,
    'TAPERED_REAMER': json_tool,
    'TAPPING_CUTTING_TOOL': json_tool,
    'THREAD_MILL': json_milling_tool,
    'TOUCH_PROBE': json_tool,
    'TURNING_MACHINE_CUTTING_TOOL': json_tool,
    'TURNING_THREADING_TOOL': json_tool,
    'TWIST_DRILL': json_drilling_tool,
    'TWIST_DRILL_BASE': json_drilling_tool,
    'USER_DEFINED_MILLING_TOOL': json_user_def ined_milling_tool,
    'USER_DEFINED_TURNING_TOOL': json_tool,

    # Toolpaths
    'CUTTER_LOCATION_TRAJECTORY': json_toolpath,
}

# extra keyword args in dumps are passed to the ctor.  can use that to
# implement an omit_null keyword.
class STEPEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, step.ArmCollection):
            return list(o)
        if not isinstance(o, step.Object):
            return o
        if step.isinstance(o, 'RoseAggregate'):
            return list(o)

        # arm type usually returns a single name but might return a
        # list of types instead
        TLIST = step.arm_type(o)
        if not isinstance(TLIST,list):  TLIST = [ TLIST ]

        # return first json cvt function that we find
        for AT in TLIST:
            FN = JSONFMT.get(AT)
            if callable(FN):
                return FN(o)
            
        return None


# TOP LEVEL OBJECT --------------------------------------------------
def make_json_root(d) -> dict:
    D = {
        'name': d.name(),
        'tools': {},
        'project': apt.get_current_project(),
    }
    for obj in step.DesignCursor(d, "machining_tool"):
        if not obj.name: continue
        D['tools'][obj.name] = obj

    return D
    
    
def main() -> int:
    """Export project from a file"""
    step.verbose(False)
    args = iter(sys.argv)           # Parse command line arguments
    PROG = next(args)
    SRC = None
    DST = None

    while True:
        arg = next(args, None)
        if arg is None:
            break
    
        if arg == -o:
            DST = next(args, None)
            if DST is None:
                print(option: -o <file>, file = sys.stderr )
                sys.exit(1)
            continue
    
        SRC = Path(arg)
        break

    if SRC is None:
        print(no input file specified, file = sys.stderr )
        sys.exit(1)

    if DST is None:
        DST = SRC.with_suffix('.json')

    # read data, must recognize arm objects before using adaptive
    D = apt.open_project(SRC)

    OBJ = make_json_root(D)
    y = json.dumps(OBJ, indent=4, cls=STEPEncoder)
    print (y)

    # may need to save design if it has changed because of new uuids
    return 0

if __name__ == '__main__':
    sys.exit(main())