Primo script Python: da profilo 2D a mesh 3D

Indice

Target
GX-TXT 0.9.8.10-dev-031
Viewer
Python Script Workspace
Runner
analysis.python_script
Livello
principiante/intermedio

Il tutorial parte da una curva 2D descritta da punti discreti, esegue un fit numerico, costruisce un profilo chiuso simmetrico, lo estrude lungo Z, triangola pareti e testate e pubblica la geometria risultante come Dataset geometry.mesh in formato VTP. La mesh può quindi essere aperta direttamente in Native 3D.

Workflow completo dal Dataset 2D alla mesh 3D
Dal Dataset di punti alla mesh VTP registrata nel Dataset Registry e visualizzabile con Native 3D.

1. Obiettivo e file dell'esercizio

Il flusso completo è:

2D point Dataset
      ↓
Python Script Workspace
      ↓
Canonical Dataset API
      ↓
NUMERIC_API_v1 least-squares fit
      ↓
dense fitted 2D profile
      ↓
Dataset Publication API v2
      ↓
mirror profile into closed section
      ↓
extrude along Z
      ↓
triangle mesh
      ↓
VTP PolyData
      ↓
Dataset File Publication API
      ↓
geometry.mesh Dataset
      ↓
Native 3D
Dataset di ingresso

profile_curve_points.csv

Script Python

profile_curve_to_extruded_mesh.py

Mesh VTP di riferimento

reference_extruded_profile_mesh.vtp

Risultati numerici

profile_mesh_expected.txt

Bundle completo

scarica tutti i file

2. Dataset di ingresso e modello geometrico

Il CSV contiene due variabili:

x_mm
half_height_mm

I punti descrivono la metà superiore di una sezione simmetrica. L'ipotesi geometrica esplicita del tutorial è:

upper boundary = +h(x)
lower boundary = -h(x)

Questa simmetria non va assunta per profili generici: qui fa parte del modello.

Punti del profilo superiore e loro specchio inferiore
I 13 punti di ingresso descrivono la metà superiore; la metà inferiore è ottenuta per simmetria.
Profilo fit e profilo specchiato
Il fit viene campionato su 81 punti; la parte inferiore chiude la sezione per simmetria.

3. Il fit matematico

Si normalizza la coordinata:

u = (x - x_min) / (x_max - x_min)

e si adotta il modello:

h(u) = u(1-u) * (a0 + a1*u + a2*u^2 + a3*u^3)

Il fattore u(1-u) forza:

h(0) = 0
h(1) = 0

Il problema dei coefficienti è un least squares lineare A a ≈ h. Python costruisce la matrice del modello; il servizio numerico GX-TXT risolve il sistema tramite:

NUMERIC_API_v1
→ linear_matrix
→ solve

Il provider numerico viene usato anche per valutare il profilo fit tramite linear_matrix / multiply.

Valori di riferimento con i dati forniti:
a0=18.0038, a1=7.09548, a2=-4.7417, a3=2.03288; residual norm ≈ 0.125429; condition number ≈ 223.22.
Residui del fit sui punti di ingresso
Residui del modello sui punti originali; il tutorial espone residual norm, rank e condition number per controllare la qualità numerica.

4. Preparare la Session e registrare lo script

4.1 Importare la curva

Importa profile_curve_points.csv come normale Dataset tabellare. Un ID comodo è:

profile_curve_points

Verifica che entrambe le colonne siano numeriche.

4.2 Registrare lo script

Apri il Python Script Workspace o la Script Library e importa profile_curve_to_extruded_mesh.py. Un ID adatto è:

tutorial.profile_to_mesh

Assegna lo script alla Session e selezionalo nel runner:

Analysis → Python Script

Imposta il Dataset della curva come Analysis Source primary.

5. API richieste

Lo script dichiara e verifica:

CANONICAL_DATASET_API_v2@2;
DATASET_VARIABLE_SCHEMA_API_v3@3;
NUMERIC_API_v1@1:math.numeric+complex_linear_algebra;
DATASET_PUBLICATION_API_v2@2;
DATASET_FILE_PUBLICATION_API_v1@1;
SESSION_PERSISTENCE_API_v1@1

Le stesse dipendenze vengono controllate a runtime con gxtxt_api_require(...).

6. Leggere il Dataset attraverso GX-TXT

Lo script ottiene l'identità del Dataset dal context:

ctx = gxtxt_context()
source_id = ctx["inputs"]["primary"]["dataset_id"]

Le Variable ID di x_mm e half_height_mm vengono risolte tramite DATASET_VARIABLE_SCHEMA_API_v3; i valori vengono letti con CANONICAL_DATASET_API_v2 / read_range.

Regola importante
Lo script non riapre il CSV originale. Dopo l'importazione lavora con Dataset ID e Variable ID stabili del framework.

7. Validazione dell'ingresso

Prima del fit vengono verificati:

Un ingresso scientificamente non valido interrompe il workflow prima della generazione della geometria.

8. Fit e profilo denso

8.1 Least squares

fit = gxtxt_api_call(
    "NUMERIC_API_v1",
    "linear_matrix",
    [
        "solve",
        {"real": design},
        {"real": rhs},
        {"rank_policy": "error"},
    ],
)

Per una matrice sovradeterminata full-rank l'operazione restituisce la soluzione least squares. Controlla almeno residual_norm, rank e condition_number.

8.2 Campionamento denso

Il tutorial genera 81 posizioni X e valuta il modello usando ancora NUMERIC_API_v1, questa volta con:

linear_matrix / multiply

I valori di half-height sono limitati al dominio fisico h ≥ 0 e gli estremi vengono riportati esattamente a zero.

9. Pubblicare il profilo derivato

Il profilo denso viene pubblicato come:

Dataset ID = tutorial.python.fitted_profile
contract   = table.generic
variables  = v_x, v_half_height

Si usa DATASET_PUBLICATION_API_v2. In questo modo la geometria successiva possiede un prodotto scientifico intermedio esplicito e tracciabile.

10. Costruire la sezione chiusa

La parte superiore è:

(x, +h)

La parte inferiore è:

(x, -h)

percorsa in ordine inverso. Gli estremi non vengono duplicati. Con 81 campioni del mezzo profilo si ottengono:

closed ring vertices = 160

11. Estrusione e triangolazione

11.1 Estrusione

La profondità usata dal tutorial è:

extrusion_depth = 12 mm
Z front = -6 mm
Z back  = +6 mm

Si creano due copie della sezione chiusa, una per ciascuna faccia.

11.2 Pareti

Ogni coppia di spigoli corrispondenti forma un quadrilatero, poi diviso in due triangoli.

11.3 Testate

Si aggiunge un vertice centrale su ciascuna estremità e si costruiscono triangoli a ventaglio. Questa strategia è adeguata al semplice profilo convesso del tutorial, ma non è un triangolatore generale per poligoni concavi o auto-intersecanti.

11.4 Dimensione attesa

half-profile samples = 81
closed ring vertices = 160
mesh vertices        = 322
mesh triangles       = 640
Anteprima 3D della mesh VTP estrusa
Mesh VTP di riferimento fornita nel bundle: 322 vertici e 640 triangoli.

12. Perché VTP e geometry.mesh

GX-TXT dispone già del contratto standard:

geometry.mesh

e Native 3D legge VTK XML PolyData .vtp. Non serve quindi inventare un formato CSV privato per vertici e triangoli.

Il file viene scritto completamente nella directory di output dello script, poi registrato con:

DATASET_FILE_PUBLICATION_API_v1 / publish

con:

dataset_id = tutorial.python.extruded_profile_mesh
contract   = geometry.mesh
format     = vtp
media_type = application/vnd.vtk

13. Due prodotti, due API di pubblicazione

Confronto fra Dataset Publication API e Dataset File Publication API
Il profilo è un Dataset numerico strutturato; la mesh è già un oggetto geometrico standard completo.

Profilo fit: variabili numeriche strutturate → DATASET_PUBLICATION_API_v2.

Mesh triangolare: file VTP completo → DATASET_FILE_PUBLICATION_API_v1.

Non è utile appiattire artificialmente una mesh in colonne CSV soltanto per usare il writer tabellare.

14. Provenance e persistenza

Il Dataset mesh registra la lineage verso:

original point Dataset
fitted profile Dataset

e metadata strutturati come:

geometry.kind
geometry.vertex_count
geometry.triangle_count
geometry.extrusion_depth
geometry.coordinate_unit

Al termine lo script chiama:

gxtxt_api_call(
    "SESSION_PERSISTENCE_API_v1",
    "commit",
    [],
)

Non modifica direttamente metadata/datasets.ini o manifest.ini.

15. Eseguire e verificare il risultato

Il log atteso contiene informazioni di questo tipo:

Profile coefficients: ...
Fit residual norm: ...
Design condition number: ...
Published fitted profile Dataset: tutorial.python.fitted_profile
Mesh: vertices=322 triangles=640 depth=12
Published 3D mesh Dataset: tutorial.python.extruded_profile_mesh
PASS PROFILE_MESH_TUTORIAL ...

Dopo il run controlla due Dataset:

tutorial.python.fitted_profile
tutorial.python.extruded_profile_mesh

Il secondo deve avere:

contract = geometry.mesh
format   = vtp

Apri quindi Native 3D e seleziona tutorial.python.extruded_profile_mesh. Non è necessario alcun renderer Python privato.

16. Divisione delle responsabilità

16.1 Python/NumPy

16.2 API GX-TXT

Il framework va usato per i servizi riusabili e per lo stato scientifico/persistente; operazioni elementari su liste e array restano naturalmente nel codice Python.

17. Limiti geometrici del tutorial

Questa procedura è un'estrusione semplice, non un kernel CAD. Non implementa:

lofting
sweeps along curved paths
twist
holes
multiple contour loops
booleans
B-rep topology
automatic remeshing
adaptive curvature refinement

Inoltre il modello di fit forza spessore nullo alle due estremità; la triangolazione a ventaglio delle testate è adatta solo alla geometria semplice usata qui.

18. Unità

Il Dataset di esempio usa millimetri. L'estrusione impiega la stessa unità della coordinata X, quindi:

X, Y, Z → mm

Se usi un altro Dataset, mantieni coerenti unità di profilo ed estrusione e conserva l'unità nei metadata.

19. Esperimenti successivi

19.1 Cambiare la profondità

Modifica:

extrusion_depth = 12.0

e confronta geometria e metadata pubblicati.

19.2 Aumentare la risoluzione

Modifica:

profile_samples = 81

Aumentando i campioni crescono vertici e triangoli della mesh.

19.3 Cambiare la complessità del modello

La matrice di base può essere ampliata con altri termini. Dopo ogni modifica controlla però il condition_number: un modello più flessibile non è automaticamente migliore.

20. Errori comuni

20.1 Riaprire il CSV

Dopo l'importazione usa Dataset ID e Variable ID. Leggere di nuovo il file sorgente rompe il normale flusso di identità e provenance.

20.2 Pubblicare vertici e facce come CSV arbitrario

Se il risultato è una mesh, usa geometry.mesh e un formato geometrico standard supportato.

20.3 Ignorare il conditioning

Un sistema formalmente risolvibile può essere numericamente poco affidabile. Controlla rank, residual e condition number.

20.4 Assumere sempre la simmetria

Il mirroring è un'ipotesi del modello di questo esercizio, non una regola generale.

20.5 Usare sempre la triangolazione a ventaglio

Profili concavi, con fori o più loop richiedono un triangolatore robusto.

21. Evoluzione naturale del workflow

Dopo questo esempio puoi sostituire la costruzione geometrica semplice con:

Quando il workflow diventa stabile, riutilizzato da più Session o dotato di configurazione propria, conviene promuoverlo da script registrato a normale modulo GX-TXT.

22. Modello mentale finale

Canonical scientific input
↓
public Dataset APIs
↓
Python domain logic + GX-TXT numeric service
↓
structured intermediate Dataset
↓
geometry construction
↓
standard VTP payload
↓
geometry.mesh publication
↓
Native 3D

Il punto centrale è separare il significato scientifico e la geometria costruita dallo script dai servizi che il framework già possiede: identità, numerica riusabile, publication, provenance e persistenza.

23. Sorgente completo

Apri profile_curve_to_extruded_mesh.py
"""GX-TXT tutorial: fit a 2D point curve, build a profile, extrude it,
triangulate the extrusion, and publish the 3D mesh as a canonical geometry.mesh Dataset.

Designed for the GX-TXT Python Script Workspace / analysis.python_script workflow.

SPDX-License-Identifier: GPL-3.0-or-later
"""

from pathlib import Path
import numpy as np

from gxtxt import gxtxt_api_call, gxtxt_api_require, gxtxt_context

gxtxt_api_require([
    "CANONICAL_DATASET_API_v2@2",
    "DATASET_VARIABLE_SCHEMA_API_v3@3",
    "NUMERIC_API_v1@1:math.numeric+complex_linear_algebra",
    "DATASET_PUBLICATION_API_v2@2",
    "DATASET_FILE_PUBLICATION_API_v1@1",
    "SESSION_PERSISTENCE_API_v1@1",
])

ctx = gxtxt_context()
source_id = ctx["inputs"]["primary"]["dataset_id"]
if not source_id:
    raise RuntimeError("Profile tutorial: select one primary Dataset.")

print(f"GX-TXT Python profile tutorial; source Dataset={source_id}")

schema = gxtxt_api_call("DATASET_VARIABLE_SCHEMA_API_v3", "list", [source_id])

def variable_by_canonical(items, canonical):
    for item in items:
        if item.get("canonical") == canonical:
            return item
    raise RuntimeError(
        f"Profile tutorial: Dataset {source_id} has no canonical variable {canonical!r}."
    )

x_desc = variable_by_canonical(schema, "x_mm")
h_desc = variable_by_canonical(schema, "half_height_mm")
x_id = x_desc["variable_id"]
h_id = h_desc["variable_id"]

description = gxtxt_api_call("CANONICAL_DATASET_API_v2", "describe", [source_id])
row_count = int(description["row_count"])
block = gxtxt_api_call(
    "CANONICAL_DATASET_API_v2",
    "read_range",
    [source_id, [x_id, h_id], 0, row_count],
)

x = np.asarray(block[x_id]["values"], dtype=float).reshape(-1)
h = np.asarray(block[h_id]["values"], dtype=float).reshape(-1)
good = np.isfinite(x) & np.isfinite(h)
x = x[good]
h = h[good]

if len(x) < 6:
    raise RuntimeError("Profile tutorial: at least six finite points are required.")

order = np.argsort(x)
x = x[order]
h = h[order]

if np.any(np.diff(x) <= 0):
    raise RuntimeError("Profile tutorial: X coordinates must be strictly increasing.")
if np.any(h < 0):
    raise RuntimeError("Profile tutorial: half-height must be non-negative.")

x_min = float(x[0])
x_max = float(x[-1])
length = x_max - x_min
if not (length > 0):
    raise RuntimeError("Profile tutorial: invalid X span.")

u = (x - x_min) / length

def profile_basis(values):
    values = np.asarray(values, dtype=float).reshape(-1)
    q = values * (1.0 - values)
    return np.column_stack([
        q,
        q * values,
        q * values**2,
        q * values**3,
    ])

design = profile_basis(u)
rhs = h.reshape(-1, 1)

fit = gxtxt_api_call(
    "NUMERIC_API_v1",
    "linear_matrix",
    [
        "solve",
        {"real": design},
        {"real": rhs},
        {"rank_policy": "error"},
    ],
)

coefficients = np.asarray(fit["value_real"], dtype=float).reshape(-1)
if len(coefficients) != 4:
    raise RuntimeError("Profile tutorial: unexpected coefficient count.")

print("Profile coefficients:", coefficients)
print("Fit residual norm:", fit["residual_norm"])
print("Design condition number:", fit["condition_number"])

profile_samples = 81
x_profile = np.linspace(x_min, x_max, profile_samples)
u_profile = (x_profile - x_min) / length
evaluation_matrix = profile_basis(u_profile)

evaluation = gxtxt_api_call(
    "NUMERIC_API_v1",
    "linear_matrix",
    [
        "multiply",
        {"real": evaluation_matrix},
        {"real": coefficients.reshape(-1, 1)},
    ],
)

half_height = np.asarray(evaluation["value_real"], dtype=float).reshape(-1)
half_height = np.maximum(half_height, 0.0)
half_height[0] = 0.0
half_height[-1] = 0.0

profile_dataset_id = "tutorial.python.fitted_profile"
definition = {
    "dataset_id": profile_dataset_id,
    "contract": "table.generic",
    "storage": "auto",
    "replace_owned": True,
    "producer_version": "1.0.0",
    "source_dataset_ids": source_id,
    "source_variable_ids": [x_id, h_id],
    "notes": "Dense fitted half-profile used to generate the tutorial extrusion mesh.",
}

writer = gxtxt_api_call("DATASET_PUBLICATION_API_v2", "begin", [definition])
try:
    gxtxt_api_call(
        "DATASET_PUBLICATION_API_v2",
        "declare_dimension",
        [writer, "row", len(x_profile), {}],
    )

    x_schema = {
        "dtype": "float64",
        "rank": 1,
        "dimension_ids": "row",
        "coordinate_role": "coordinate",
        "semantic_role": "independent",
        "unit": x_desc.get("unit", ""),
    }
    h_schema = {
        "dtype": "float64",
        "rank": 1,
        "dimension_ids": "row",
        "coordinate_role": "data",
        "semantic_role": "derived",
        "unit": h_desc.get("unit", ""),
    }

    gxtxt_api_call(
        "DATASET_PUBLICATION_API_v2", "declare_variable",
        [writer, "v_x", x_schema],
    )
    gxtxt_api_call(
        "DATASET_PUBLICATION_API_v2", "declare_variable",
        [writer, "v_half_height", h_schema],
    )
    gxtxt_api_call(
        "DATASET_PUBLICATION_API_v2", "write_variable",
        [writer, "v_x", x_profile, len(x_profile)],
    )
    gxtxt_api_call(
        "DATASET_PUBLICATION_API_v2", "write_variable",
        [writer, "v_half_height", half_height, len(half_height)],
    )

    published_profile = gxtxt_api_call(
        "DATASET_PUBLICATION_API_v2", "finalize", [writer]
    )
    writer = None
finally:
    if writer is not None:
        gxtxt_api_call("DATASET_PUBLICATION_API_v2", "abort", [writer])

print("Published fitted profile Dataset:", published_profile["dataset_id"])

upper = np.column_stack([x_profile, half_height])
lower = np.column_stack([
    x_profile[-2:0:-1],
    -half_height[-2:0:-1],
])
ring_xy = np.vstack([upper, lower])
ring_count = len(ring_xy)

extrusion_depth = 12.0
z_front = -0.5 * extrusion_depth
z_back = +0.5 * extrusion_depth

front_ring = np.column_stack([
    ring_xy[:, 0],
    ring_xy[:, 1],
    np.full(ring_count, z_front),
])
back_ring = np.column_stack([
    ring_xy[:, 0],
    ring_xy[:, 1],
    np.full(ring_count, z_back),
])

center_xy = np.mean(ring_xy, axis=0)
vertices = np.vstack([
    front_ring,
    back_ring,
    [center_xy[0], center_xy[1], z_front],
    [center_xy[0], center_xy[1], z_back],
])

front_center = 2 * ring_count
back_center = front_center + 1

triangles = []
for i in range(ring_count):
    j = (i + 1) % ring_count
    triangles.append((i, j, ring_count + j))
    triangles.append((i, ring_count + j, ring_count + i))

for i in range(ring_count):
    j = (i + 1) % ring_count
    triangles.append((front_center, j, i))
    triangles.append((back_center, ring_count + i, ring_count + j))

triangles = np.asarray(triangles, dtype=np.int64)

if np.min(triangles) < 0 or np.max(triangles) >= len(vertices):
    raise RuntimeError("Profile tutorial: generated triangle index out of range.")

print(
    f"Mesh: vertices={len(vertices)} triangles={len(triangles)} "
    f"depth={extrusion_depth:g}"
)

mesh_file = Path(ctx["output_dir"]) / "extruded_profile_mesh.vtp"

def write_ascii_vtp(path, points, faces):
    connectivity = " ".join(
        str(int(index)) for face in faces for index in face
    )
    offsets = " ".join(
        str(3 * (i + 1)) for i in range(len(faces))
    )
    coordinates = " ".join(
        f"{float(value):.12g}" for point in points for value in point
    )

    document = f"""<?xml version="1.0"?>
<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian">
  <PolyData>
    <Piece NumberOfPoints="{len(points)}"
           NumberOfVerts="0"
           NumberOfLines="0"
           NumberOfStrips="0"
           NumberOfPolys="{len(faces)}">
      <Points>
        <DataArray type="Float64" NumberOfComponents="3" format="ascii">
          {coordinates}
        </DataArray>
      </Points>
      <Polys>
        <DataArray type="Int32" Name="connectivity" format="ascii">
          {connectivity}
        </DataArray>
        <DataArray type="Int32" Name="offsets" format="ascii">
          {offsets}
        </DataArray>
      </Polys>
    </Piece>
  </PolyData>
</VTKFile>
"""
    path.write_text(document, encoding="utf-8")

write_ascii_vtp(mesh_file, vertices, triangles)

mesh_dataset_id = "tutorial.python.extruded_profile_mesh"
mesh_result = gxtxt_api_call(
    "DATASET_FILE_PUBLICATION_API_v1",
    "publish",
    [{
        "dataset_id": mesh_dataset_id,
        "contract": "geometry.mesh",
        "format": "vtp",
        "source_file": str(mesh_file),
        "storage_kind": "binary",
        "media_type": "application/vnd.vtk",
        "producer_version": "1.0.0",
        "source_dataset_ids": [source_id, profile_dataset_id],
        "source": "Python Script Workspace profile extrusion tutorial",
        "provenance": (
            "Triangle mesh created by fitting the source half-profile through "
            "NUMERIC_API_v1, mirroring it into a closed section, and extruding "
            "the section along Z."
        ),
        "notes": (
            f"vertices={len(vertices)}; triangles={len(triangles)}; "
            f"extrusion_depth={extrusion_depth:g}"
        ),
        "structured_metadata": {
            "geometry.kind": "extruded_profile_triangle_mesh",
            "geometry.vertex_count": str(len(vertices)),
            "geometry.triangle_count": str(len(triangles)),
            "geometry.source_profile_dataset": profile_dataset_id,
            "geometry.extrusion_depth": f"{extrusion_depth:.12g}",
            "geometry.coordinate_unit": x_desc.get("unit", ""),
        },
        "replace_owned": True,
    }],
)

print("Published 3D mesh Dataset:", mesh_result["dataset_id"])
print("Mesh SHA-256:", mesh_result["sha256"])

gxtxt_api_call("SESSION_PERSISTENCE_API_v1", "commit", [])

report = Path(ctx["output_dir"]) / "profile_mesh_report.txt"
report.write_text(
    "\n".join([
        "GX-TXT Python profile-to-mesh tutorial",
        "======================================",
        "",
        f"Source Dataset: {source_id}",
        f"Fitted profile Dataset: {profile_dataset_id}",
        f"Mesh Dataset: {mesh_dataset_id}",
        f"Input points: {len(x)}",
        f"Dense profile samples: {len(x_profile)}",
        f"Ring vertices: {ring_count}",
        f"Mesh vertices: {len(vertices)}",
        f"Mesh triangles: {len(triangles)}",
        f"Extrusion depth: {extrusion_depth:g} {x_desc.get('unit', '')}",
        f"Fit residual norm: {fit['residual_norm']}",
        f"Condition number: {fit['condition_number']}",
        "",
    ]),
    encoding="utf-8",
)

print("PASS PROFILE_MESH_TUTORIAL", source_id, profile_dataset_id, mesh_dataset_id)

24. Guide collegate