NX Journal로 부품별 질량·밀도 CSV 내보내기|누락값과 단위 검증

조립품에 들어가는 부품이 수십 개만 넘어가도 하나씩 Measure Body를 실행해 질량을 기록하는 방식은 누락을 확인하기 어렵습니다. 같은 부품이 세 번 들어갔는데 목록에는 한 번만 적거나, 밀도가 지정되지 않은 부품을 정상 질량에 합산하면 BOM과 실제 설계 결과가 어긋날 수 있습니다.

이 글에서는 NX Open Python Journal로 조립품의 발생 부품(Occurrence)을 순회하면서 질량·부피·유효 밀도를 CSV로 내보내는 실습을 다룹니다.

그리고 부품 형상을 변경하지 않고 물리 재질 누락, 미로딩 부품, 억제된 부품을 별도 상태로 남기도록 구성했습니다.

실행 검증 범위

아래 Journal은 공개 NX Open API에 맞춰 제작하고 Python 구문 검사까지 완료한 검증용 예제입니다. 이 환경에서는 Siemens NX 프로그램으로 실제 실행하거나 NX 버전별 결과를 측정하지 않았습니다. 수치가 기재된 표와 CSV는 독립 계산한 가상 기대값입니다. 최초 실행은 백업된 비기밀 테스트 어셈블리에서 진행하세요.

NX Journal 질량 CSV 자동화의 목표

핵심은 단순히 질량 숫자를 빼내는 것이 아니라 어떤 부품에서 어떤 조건으로 구한 값인지 재검증할 수 있도록 출력하는 것입니다. 따라서 계산에 실패한 행을 지우지 않고 상태 코드와 사유를 남깁니다.

데이터처리 원칙설계 검증 목적
부품 경로어셈블리 발생 경로를 기록같은 Part의 반복 배치를 구분
부피솔리드 형상의 mm³ 측정단위 혼동 확인
질량NX에 지정된 물리 재질을 전제로 kg 측정밀도 누락의 숫자 오염 방지
밀도질량÷부피로 계산한 유효 밀도 (kg/m³)재질 카드 값과 비교할 단서 제공
실패 사유정상 합계와 분리 기록수량·재질·로딩 문제 추적

NX 자체에서 단일 부품의 재질과 질량을 확인하는 과정은 NX 밀도 입력·질량·부피 측정 가이드를 먼저 참고하면 이해가 쉽습니다.

이번 자동화는 그 수동 측정을 반복 배치된 어셈블리 전체로 확장하는 작업입니다.

적용 가능한 NX Open API와 실행 조건

이 예제는 NXOpen.Assemblies.Component.GetChildren()으로 부품 구조를 순회하고, 물리 재질 지정 상태는 PhysicalMaterials.AskMaterialOfObject()로 조회합니다.

수치는 MeasureManager.NewMassProperties()의 Volume·Mass를 읽어 계산합니다. 해당 API는 NX 릴리스와 Python 바인딩에 따라 동작이 다를 수 있습니다.

예상 대상은 NX 2206·2212·2306 이후 계열의 NX Open Python 인터페이스이지만, 실제 보장되는 지원 버전 목록이 아닙니다.

사용 중인 NX 빌드 번호, Python Journal 실행 권한, NX Open Python Author 관련 라이선스, Teamcenter 관리 여부를 확인해야 합니다. NX 라이선스 구성별 사용 조건은 관리자가 확인해 주세요.

가상 4부품 조립품으로 테스트하기

본격적으로 회사 제품을 검사하기 전에 정답을 계산하기 쉬운 테스트 어셈블리부터 권장합니다.

NX Journal로 부품별 질량·밀도 CSV 내보내기|누락값과 단위 검증 1

아래 수치는 NX에서 측정한 결과가 아니라 단순 직육면체 형상과 가정한 밀도로 산술 계산한 기대값입니다.

부품단순 형상(mm)물리 밀도(가정)발생 수기대 질량
A_AL20 × 30 × 102,700 kg/m³2개0.0324 kg
B_STEEL10 × 10 × 107,800 kg/m³1개0.0078 kg
C_NO_MATERIAL10 × 10 × 10물리 재질 미지정1개합계 제외
정상 합계A 2개 + B 1개–3개0.0402 kg

학습용 STEP 어셈블리는 CAD 형상만 포함합니다.

STEP 색상이나 부품 이름만으로 NX 물리 재질이 자동 지정되었다고 판단하면 안 됩니다.

NX로 가져온 후 A 부품에는 지정한 알루미늄 재질, B에는 강철 재질을 각각 할당하고 C는 재질 미지정으로 남겨 둡니다.

00 대표이미지 NX Journal 질량 밀도 CSV

정확히 동일한 2,700·7,800 kg/m³ 값을 가진 라이브러리 재질 또는 검증된 테스트 재질을 사용해야 기대값과 비교할 수 있습니다.

01 본문이미지 NX 4부품 샘플 조립품

NX STEP 가져오기 옵션에 따라 원래의 어셈블리 구조가 개별 파트 대신 다중 솔리드 단일 Part로 들어올 수 있습니다.

Assembly Navigator에 A 두 번, B 한 번, C 한 번으로 4개 Occurrence가 보이는지 먼저 점검하세요. 그렇지 않으면 파일을 개별 파트로 나누어 어셈블리에 배치하는 방식으로 샘플을 구성해야 합니다.

NX Journal 실행 및 CSV 저장

1. 읽기 전용 대상 파일 열기

  1. 실행 전 파일을 복제하고, 원본 모델의 저장 상태 및 물리 재질을 확인합니다.
  2. 테스트 어셈블리를 NX에서 엽니다. 억제된 부품·미로딩 부품이 있다면 작업 상태를 기록합니다.
  3. 가능하면 완전 로딩 상태에서 시작합니다. 코드가 부품을 자동 로드하거나 억제를 변경하지 않도록 설계되어 있습니다.

2. Python Journal 실행

NX에서 Tools → Journal → Play에 해당하는 저널 재생 메뉴를 열고 아래 Python 파일을 지정합니다. UI 위치와 표기는 릴리스·리본 사용자화에 따라 다를 수 있습니다.

Python Journal 파일
NX_Journal_부품별_질량_밀도_CSV_검증용.py

코드 본문은 아래 접기 영역에서 모두 복사할 수 있습니다. 배포용 .py 파일은 WordPress 미디어 업로드 후 실제 다운로드 URL을 연결하세요.

파일의 OUTPUT_DIR은 실행 사용자 홈 폴더입니다. CSV 파일은 nx_mass_audit_날짜_시간.csv 형식으로 생성합니다. 동일 파일에 덮어쓰지 않으며 UTF-8 BOM으로 저장해 엑셀 한글 깨짐을 줄였습니다. 다만 폴더 쓰기 권한은 확인해야 합니다.

3. 전체 코드 보기 및 복사

NX Open Python 전체 Journal 코드 펼치기
# -*- coding: utf-8 -*-
"""NX Open Python Journal - assembly occurrence mass audit (read-only design).

Test target: NX 2206/2212/2306+ API surfaces; NOT run in NX in this environment.
Does not create expressions/features, modify bodies, assign materials, or save parts.
Reports measured NX physical-material mass (kg), volume (mm^3), derived density
(kg/m^3) only for solids with an explicitly assigned physical material.

Run within NX: Tools > Journal > Play (UI labels may differ by release).
Output: $HOME/nx_mass_audit_YYYYMMDD_HHMMSS.csv
IMPORTANT: trial on a backup/non-confidential assembly first.
"""
import csv
import datetime
import math
import os

import NXOpen

ACCURACY = 0.99
OUTPUT_DIR = os.path.expanduser("~")
FIELDS = [
    "component_path", "part_number", "part_units", "occurrence_count",
    "body_count", "valid_body_count", "material_names", "volume_mm3",
    "density_kg_m3", "mass_kg", "partial_mass_kg", "status", "reason"
]


def safe_text(value):
    """Prevent CSV formula execution when opened in spreadsheet programs."""
    s = str(value if value is not None else "").replace("\x00", "")
    if s and s[0] in ("=", "+", "-", "@", "\t", "\r", "\n"):
        return "'" + s
    return s


def default_row(path, part_name="", unit_name=""):
    return dict(component_path=path, part_number=part_name,
                part_units=unit_name, occurrence_count="1", body_count="0",
                valid_body_count="0", material_names="", volume_mm3="",
                density_kg_m3="", mass_kg="", partial_mass_kg="",
                status="", reason="")


def number(value):
    return "{:.9g}".format(value) if value is not None and math.isfinite(value) else ""


def get_units(part):
    uc = part.UnitCollection
    return [uc.FindObject(name) for name in (
        "SquareMilliMeter", "CubicMilliMeter", "Kilogram", "MilliMeter", "Newton")]


def get_part_units(part):
    try:
        return str(part.PartUnits)
    except Exception:
        return "unknown (measured output: mm3/kg)"


def read_bodies(part):
    return [body for body in part.Bodies if body.IsSolidBody]


def read_physical_material(part, body):
    """No fallback density: if unsupported/unassigned, report explicitly."""
    try:
        mat = part.MaterialManager.PhysicalMaterials.AskMaterialOfObject(body)
    except AttributeError as exc:
        return None, "MATERIAL_API_UNAVAILABLE: " + str(exc)
    except Exception as exc:
        return None, "MATERIAL_QUERY_FAILED: " + str(exc)
    if mat is None:
        return None, "MISSING_MATERIAL"
    try:
        return str(mat.Name), None
    except Exception:
        return "assigned_material", None


def measure_one(part, body, units):
    """Temporary measurement object; no CreateFeature and no saved part changes."""
    measurement = None
    try:
        measurement = part.MeasureManager.NewMassProperties(units, ACCURACY, [body])
        return float(measurement.Volume), float(measurement.Mass)
    finally:
        if measurement is not None:
            measurement.Dispose()


def audit_part(part, path):
    row = default_row(path, safe_text(part.Name), get_part_units(part))
    try:
        bodies = read_bodies(part)
        row["body_count"] = str(len(bodies))
        if not bodies:
            row["status"] = "NO_SOLID_BODY"
            row["reason"] = "No solid bodies in prototype part"
            return row
        units = get_units(part)
    except Exception as exc:
        row["status"] = "PART_READ_FAILED"
        row["reason"] = safe_text(str(exc))
        return row

    volumes, masses, materials, errors = [], [], [], []
    for i, body in enumerate(bodies):
        name, problem = read_physical_material(part, body)
        if problem:
            errors.append("body{}:{}".format(i + 1, problem))
            continue
        try:
            vol, mass = measure_one(part, body, units)
            if not (math.isfinite(vol) and math.isfinite(mass) and vol > 0 and mass >= 0):
                raise ValueError("Invalid volume or mass")
            volumes.append(vol)
            masses.append(mass)
            materials.append(name)
        except Exception as exc:
            errors.append("body{}:MEASURE_FAILED:{}".format(i + 1, str(exc)))

    row["valid_body_count"] = str(len(volumes))
    row["material_names"] = safe_text("; ".join(sorted(set(materials))))
    if volumes:
        row["partial_mass_kg"] = number(sum(masses))
    if errors:
        row["status"] = "INCOMPLETE"
        row["reason"] = safe_text(" | ".join(errors))
        return row  # no total mass/density for a partially measured part

    volume_sum, mass_sum = sum(volumes), sum(masses)
    row["volume_mm3"] = number(volume_sum)
    row["mass_kg"] = number(mass_sum)
    # effective density is mass-weighted at the occurrence level, NOT a material card value.
    row["density_kg_m3"] = number(mass_sum / (volume_sum * 1e-9))
    row["status"] = "OK_MIXED_MATERIAL" if len(set(materials)) > 1 else "OK"
    row["reason"] = "effective density for multiple materials" if len(set(materials)) > 1 else ""
    return row


def walk_component(comp, path, rows, cache, visited=None):
    # Each occurrence is recorded, but prototype properties can be cached.
    name = safe_text(getattr(comp, "DisplayName", getattr(comp, "Name", "component")))
    here = path + "/" + name if path else name
    try:
        if comp.IsSuppressed:
            row = default_row(here)
            row.update(status="SUPPRESSED", reason="Excluded by current arrangement")
            rows.append(row)
            return
    except Exception as exc:
        row = default_row(here)
        row.update(status="SUPPRESSION_UNKNOWN", reason=safe_text(str(exc)))
        rows.append(row)
        return
    try:
        children = list(comp.GetChildren())
    except Exception as exc:
        row = default_row(here)
        row.update(status="CHILD_QUERY_FAILED", reason=safe_text(str(exc)))
        rows.append(row)
        return
    try:
        proto = comp.Prototype
        part = proto.OwningPart
        if part is None:
            raise ValueError("Prototype owning part is not loaded")
        key = int(part.Tag)
        if key not in cache:
            cache[key] = audit_part(part, "")
        report = dict(cache[key])
        report["component_path"] = here
        # Skip empty subassembly container but continue into its children.
        if not (children and report["status"] == "NO_SOLID_BODY"):
            rows.append(report)
    except Exception as exc:
        row = default_row(here)
        row.update(status="UNLOADED_COMPONENT", reason=safe_text(str(exc)))
        rows.append(row)
    for child in children:
        walk_component(child, here, rows, cache)


def main():
    session = NXOpen.Session.GetSession()
    lw = session.ListingWindow
    lw.Open()
    work = session.Parts.Work
    if work is None:
        lw.WriteLine("[ERROR] Open a part or an assembly first.")
        return
    rows = []
    root = work.ComponentAssembly.RootComponent
    if root is None or not list(root.GetChildren()):
        rows.append(audit_part(work, safe_text(work.Name)))
    else:
        cache = {}
        for child in root.GetChildren():
            walk_component(child, safe_text(work.Name), rows, cache)

    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
    target = os.path.join(OUTPUT_DIR, "nx_mass_audit_" + timestamp + ".csv")
    with open(target, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, FIELDS, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)
    passed = [r for r in rows if r["status"] in ("OK", "OK_MIXED_MATERIAL")]
    lw.WriteLine("[DONE] CSV written: " + target)
    lw.WriteLine("[COUNT] rows={}, fully-measured={}".format(len(rows), len(passed)))
    lw.WriteLine("[INFO] All numerical mass results are NX-measured, not independently verified.")
    lw.WriteLine("[INFO] Failed/suppressed/unloaded rows excluded from total.")
    lw.WriteLine("[TOTAL] kg={:.9g}".format(sum(float(r["mass_kg"]) for r in passed)))


if __name__ == "__main__":
    main()

이 Journal에서 하지 않는 작업

  • 재질을 임의로 지정하거나 빈 값에 강철 밀도를 채우지 않습니다.
  • 부품을 억제 해제하거나 언로드된 형상을 자동으로 로드하지 않습니다.
  • 측정값을 모델 속성이나 표현식에 기록하거나 PRT 파일을 저장하지 않습니다.
  • 영구 Measure Feature를 생성하지 않으며 임시 측정 개체는 Dispose() 처리합니다.

CSV 결과는 정상값과 오류값을 분리해서 확인

02 본문이미지 NX CSV 결과 검증

다음은 샘플 형상에 가정한 물리 재질이 정확하게 지정되고 NX 측정이 성공했다는 조건부 예상 결과입니다. 이 CSV는 실제 NX 실행 캡처나 실측 자료가 아닙니다.

component_pathmass_kgdensity_kg_m3status
ASSEMBLY/A_AL_010.01622700OK
ASSEMBLY/A_AL_020.01622700OK
ASSEMBLY/B_STEEL_010.00787800OK
ASSEMBLY/C_NO_MATERIAL_01(빈칸)(빈칸)INCOMPLETE

정상행 3개와 재질 누락행 1개를 구분한 뒤 정상 질량만 합하면 0.0402 kg입니다. 실제 검증은 Journal이 생성한 CSV의 질량, 상태, 바디 개수와 이 예상표가 일치하는지 비교하는 방식으로 진행합니다.

설명용 흐름도 · 실제 NX 화면 아님AssemblyOccurrence 순회재질·측정 확인바디 · kg · mm³OK완료값 → 정상 합계INCOMPLETE누락 사유 → 합계 제외

혼합 물리 재질의 밀도는 어떻게 표시되나요?

부품의 여러 솔리드 바디가 각기 다른 물리 재질을 가진 경우 질량은 각 바디 측정값을 합산하고, CSV의 밀도는 전체 질량 ÷ 전체 부피로 계산한 유효 밀도를 기록합니다.

이때 상태는 OK_MIXED_MATERIAL로 표시합니다. 해당 값은 특정 한 재질의 원재료 밀도가 아닙니다.

ADVERTISEMENT

오류 코드별 조치 방법

status발생 원인먼저 확인할 작업
INCOMPLETE일부 바디 재질 미지정·측정 실패reason 칸의 body 번호 확인, 물리 재질 지정 확인
NO_SOLID_BODY스케치·서피스만 있거나 빈 Part솔리드 바디 유무 확인
UNLOADED_COMPONENT프로토타입 Part 접근 실패어셈블리 로딩 상태 확인 후 재실행
SUPPRESSED활성 Arrangement에서 억제BOM 포함 정책을 정하고 별도 관리
MATERIAL_API_UNAVAILABLENX 릴리스에서 API 호출 불가설치 버전의 API 문서로 대응 코드 확인
PART_READ_FAILED바디 또는 단위 객체 접근 실패파트 손상·측정 단위·사용 권한 확인

실제 양산 BOM 질량과 비교할 때는 ① 부품 발생 수 ② 재질 누락 ③ 억제 부품 포함 여부 ④ 미로딩 부품 ⑤ 다중 솔리드 재질 지정 여부를 먼저 점검해야 합니다. 단순히 0.0402 kg처럼 합계가 맞는지만 보는 검사는 충분하지 않습니다.

부품표와 수량 자체를 NX에서 확인하는 방법은 NX BOM 파트리스트 작성·엑셀 내보내기를, 조립품의 무게중심과 질량중심을 더 자세히 검토하려면 NX Center of Mass 측정을 참고할 수 있습니다.

실무 적용 전 최종 검증

  1. 기본 샘플 검사: A가 2행, B가 1행, C가 1행인지 확인합니다.
  2. 단위 검사: mm³, kg, kg/m³로 작성된 컬럼과 NX Measure 결과를 대조합니다.
  3. 누락 검사: C의 mass_kg는 빈칸이며 reason에는 누락 사유가 기록되는지 확인합니다.
  4. 정합성 검사: 정상 합계 0.0402 kg과 개별 발생 부품의 합산 결과를 비교합니다.
  5. 재현성 검사: 같은 NX 빌드와 동일 조립품에서 두 번 실행해 결과를 대조합니다.
  6. 변경 확인: 실행 전후 PRT 저장 상태 및 형상·속성이 변경되지 않았는지 검토합니다.

또한 STEP 변환 과정에서 특정 Part가 다중 바디로 합쳐졌다면 발생 부품 수와 실제 파일 구조를 먼저 바로잡아야 합니다. Teamcenter 관리 어셈블리에서는 파일 로딩 정책 및 BOM 뷰 설정에 따라 순회 범위가 달라질 수 있습니다.

자주 묻는 질문

Journal을 실행하면 NX 모델이 바뀌나요?

제공한 스크립트에는 저장·형상 편집·재질 지정 명령을 넣지 않았습니다. 다만 사용 중인 NX에서 임시 측정 객체의 동작과 저장 상태를 테스트한 뒤 실제 프로젝트에 적용해야 합니다.

밀도가 없는 부품은 0 kg으로 출력하나요?

아닙니다. 물리 재질이 확인되지 않으면 해당 부품은 INCOMPLETE로 기록하고 mass_kg은 빈칸으로 남깁니다. 질량 0과 누락은 다른 의미입니다.

같은 볼트가 열 번 들어가면 CSV는 몇 행인가요?

동일 Part가 서로 다른 Occurrence로 열 번 배치되었다면 발생 경로 기준으로 열 행을 기록하도록 설계했습니다. 단, 가상 부품·비형상 컴포넌트 등은 별도 정책 확인이 필요합니다.

미터와 밀리미터 파일이 섞여도 괜찮나요?

MeasureManager에 결과 단위를 명시적으로 전달해 mm³와 kg로 받도록 작성했습니다. 실제 NX 버전의 단위 해석이 맞는지 mm·inch 샘플을 교차 검증해야 합니다.

물리 재질이 없지만 Part 속성에 재질명이 적혀 있으면요?

단순 문자 속성의 이름만으로 밀도값을 추정하지 않습니다. 실제 바디에 할당된 물리 재질을 API로 확인하지 못하면 누락으로 처리합니다.

혼합 재질인 부품의 밀도는 어떤 값인가요?

전체 질량을 전체 부피로 나눈 유효 밀도입니다. 단일 재질 카드에 쓰인 고유 밀도로 해석하면 안 되며 CSV의 status로 구분합니다.

Journal이 Teamcenter에서도 동작하나요?

NX Open API의 일부는 관리 모드에서 사용할 수 있지만 실제 부품 로딩·권한·라이선스 환경에 따라 검증이 필요합니다. 본 스크립트는 Teamcenter 환경에서 실측 테스트하지 않았습니다.

CSV와 BOM 질량 합계가 다르면 무엇부터 볼까요?

중복 Occurrence 수, 재질 미지정 행, 억제·미로딩 부품, 다중 바디와 BOM의 비형상 항목 포함 여부부터 비교하는 편이 빠릅니다.

참고 자료와 적용 범위

Siemens는 NX의 Assembly Mass Properties를 이용해 질량·밀도·부피와 부품 단위 데이터를 검증하는 워크플로를 안내합니다.

NX Open Python API는 솔리드 측정 및 어셈블리 트리 조회를 자동화하는 수단이며, 버전별 API 변경 가능성이 있으므로 현장 표준 빌드에서 재시험한 뒤 업무에 적용해야 합니다.

저라면 첫 적용에서 4부품 샘플의 기대값과 실제 출력 결과를 비교한 뒤, 작은 사내 비기밀 조립품으로 범위를 넓히겠습니다. 누락된 재질과 부품 경로를 함께 남겨야 단순 자동화가 아니라 설계 검증에 사용할 수 있는 CSV가 됩니다.

ADVERTISEMENT

DecreYellow

제품디자인과 3D CAD 설계 실무를 기반으로 IT, 윈도우 오류 해결, CAD 소프트웨어, 디자이너 세금, 프리랜서·개인사업자 실무 정보를 정리하고 있습니다. 단순한 이론 설명보다 실제 업무와 블로그 운영, 프로그램 사용 경험을 바탕으로 초보자도 이해하기 쉽게 문제 원인과 해결 방법을 풀어내는 것을 목표로 합니다.

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다