Python API Reference

SurfCalc can be driven from Python in three ways:

  • GUISurfCalcAPI (section 1) — the in-app Engineering Console API, driving a currently loaded/running SurfCalc session (accessed as surfcalc.<method>(...) from View > Engineering Console). Use this to script actions against the project you already have open in the GUI.
  • Headless RPC mode (section 2) — for driving SurfCalc from your own external Python process (e.g. an optimization loop) with no GUI window open at all — SurfCalc.exe --serve-stdio plus the companion surfcalc_client package.
  • Macro recording (section 3) — GUI actions captured as a replayable script you can re-run later.

Recorded load/calculation actions for the covered modules generate calls into GUISurfCalcAPI, so a recorded macro and a Console command use identical code paths.

Covered modules: Load Case Combination, Influence Matrix, Surface Deformation, Active Optics, and Bending Modes. Image Motion is unreleased and not covered.


1. GUISurfCalcAPI (Engineering Console)

Accessed from View > Engineering Console as surfcalc.<method>(...), scripting the currently loaded GUI session rather than an external file.

Method Purpose
switch_tab(module_name) Switch the active module ("surface_def", "influence_matrix", "active_optics", "load_cases", "bending_modes", "image_motion"). Returns True/False.
calculate() Run the calculation for whichever module is currently active (fit / influence-matrix compile / active-optics correction / load case combination / bending-modes extract). On Surface Deformation, Active Optics, Load Case Combination, and Bending Modes, if an FEA node set is already auto-selected by the optical-keyword heuristic (optical/opt/os/mirror) but has not been previewed yet, calculate() loads that set first rather than warning. A Default (All nodes) selection still needs an explicit preview. Returns True/False.
load_bm_preview_set() Bending Modes FEA only: load all modes for the selected node set/step (GUI Load & Preview Set). Optional before calculate() on FEA imports when the selected set is a keyword match — calculate() will load it itself in that case. Returns True/False.
apply_alignment() Apply the Active Optics target mesh alignment (Active Optics only). Returns True/False.
get_tab_names() List all module type identifiers.
select_instance(inst_id) Switch the active project instance.
add_instance(module_type) Create a new instance of the given module type; returns its new instance id.
duplicate_instance(inst_id) Duplicate an existing instance; returns the new instance id.
get_instances() List all current project instances.
import_file(solver_type, filepath, target="surface_def", units_coords="mm") Import a data file for the given module target ("surface_def", "load_cases", ...). solver_type is "csv", "abaqus", "ansys", "nastran", "zemax_grid_sag", or "codev_interferogram". Returns True.
correct_import_units(target, coords_unit, force_unit=None, actuator_type=None) Apply a post-import unit correction. Returns True.
export_surface_csv(filepath) Write the current Surface Deformation displacement field as a re-importable CSV, in metres (Surface Deformation only). Returns True/False.
export_report(filepath) Write the current result as a self-contained, SurfCalc-branded HTML analysis report and open it in your default browser. Works on whichever tab is active: Surface Deformation (after a fit) — three XY-plane viewport screenshots, fit setup, metrics, RBM, coefficients, and load-case provenance; Active Optics (after a correction) — four XY-plane viewport screenshots (Target/Fit/Residual/Overlay), correction quality, solve setup, tolerance results, and the actuator table. Print to PDF from the opened tab to attach it to a design review. Returns True/False.
send_to_active_optics() Write the fitted Surface Deformation surface error as an Active Optics target, switch to a new Active Optics instance, and load it. Requires a completed fit. Returns the new instance id, or False.
auto_detect_hole_diameter() Fill Mask Inner Hole diameter from the loaded surface / combined result (Surface Deformation or Load Case Combination). Same as the sidebar Auto button. Returns True/False.
surfcalc.switch_tab("surface_def")
surfcalc.import_file("csv", "displacements.csv", target="surface_def")
surfcalc.auto_detect_hole_diameter()  # optional; enables mask + fills hole_diam
surfcalc.calculate()

1a. Load Case Combination (switch_tab("load_cases"))

Method Purpose
add_load_case(solver_type, filepath, units_coords="mm") Append another result file as a load case (rather than replacing the list). With no arguments, creates an empty slot instead. Returns True/False.
select_load_case(idx) Load that case's import settings back into Data Ingestion, so a subsequent import_file replaces that case's model in place. Returns True/False.
duplicate_load_case(idx) Copy the case at idx (file, import selection, units, scale, alignment and its loaded mesh) into a new slot right after it, added disabled and selected — so a following import_file(..., target="load_cases") replaces its model, e.g. the same model at a different load step. The GUI form is right-click a case → Duplicate. Returns True/False.
remove_load_case(idx) Remove the load case at idx. Returns True/False.
update_load_case(idx, field, value) Set a case's "scale", "enabled", or "name". Returns True/False.
set_reference_case(idx) Choose which case every other one is resampled/registered onto. Pass -1 for automatic (most refined mesh). Returns True/False.
set_combine_mode(mode) "superpose" (signed sum) or "rss" (envelope). Returns True/False.
get_load_cases() List the current load cases.
export_combined_csv(filepath) Write the current combined displacement field as a re-importable CSV, in metres. Returns True/False.
export_lc_sweep_csv(filepath) Write the current Grid Sweep results table as a CSV (display units, matching the on-screen columns). Returns True/False.
send_to_surface_deformation() Create a Surface Deformation instance fed by the current combined field. Repeat to feed several fits with different bases from the same combination. Returns the new instance id, or False.
send_combination_to_active_optics() Create an Active Optics instance fed by the current combined field as its target. Returns the new instance id, or False.

Combining a STOP load-case set (gravity + thermal + wind), then fitting it:

surfcalc.switch_tab("load_cases")
surfcalc.import_file("csv", "gravity.csv", target="load_cases")
surfcalc.add_load_case("csv", "thermal.csv")
surfcalc.update_load_case(1, "scale", 0.6)
surfcalc.add_load_case("csv", "wind.csv")
surfcalc.set_combine_mode("rss")
surfcalc.calculate()  # forces a recombine -- the GUI itself recombines live
surfcalc.send_to_surface_deformation()
surfcalc.calculate()  # fits the newly created Surface Deformation instance

2. Headless RPC Mode (--serve-stdio)

For driving SurfCalc from your own external Python process — e.g. an optimization loop that calls run_active_optics many times over varying parameters — with no GUI window needed, start SurfCalc in headless RPC mode:

SurfCalc.exe --serve-stdio

This starts no local web server, browser, or webview — it reads one JSON request per line from stdin and writes one JSON response per line to stdout, then exits cleanly on EOF. It is meant to be launched as a subprocess and kept alive for an entire session, not invoked once per call.

Use the companion surfcalc_client package rather than talking to the pipe directly:

from surfcalc_client import SurfCalcSession

with SurfCalcSession(exe_path=r"C:\Program Files\SurfCalc\SurfCalc.exe") as session:
    result = session.run_active_optics(inf_path, target_path, alpha=0.05)
    print(result["rms_residual"])

A failed call raises surfcalc_client.SurfCalcRPCError without ending the session, so one bad call in a parameter sweep doesn't cost you the whole run. One SurfCalcSession = one dedicated SurfCalc subprocess = one warm session (its own startup cost is paid once, not per call); it does not support multiple concurrent clients sharing a single running server. See the package's own README for the full install/usage guide, including a scipy.optimize example.

Licensing in headless mode

A headless session enforces the same entitlement gate as the GUI (see Software Licensing), and gets its licence from one of two places.

If you activated SurfCalc through the GUI on this machine, there is nothing to configure. The stored activation is restored automatically at startup, exactly as it is for the desktop app.

That store is per-user, though, so it is often absent on the hosts headless mode is actually for — a service account, a container, a CI runner, or any machine where nobody has opened the GUI. Supply the key through the environment there:

Variable Value
SURFCALC_LICENSE_KEY The licence key itself (a path to a licence file also works)
SURFCALC_LICENSE_FILE Path to a .lic/.key/.json/.txt licence file

SURFCALC_LICENSE_KEY takes precedence if both are set.

export SURFCALC_LICENSE_KEY="key/eyJhbGci...signature"
python -c "from surfcalc_client import SurfCalcSession; ..."

Three things worth knowing:

  • The key is used for that session only. Unlike clicking Activate in the GUI, nothing is written to disk — the environment is already the durable source of the key in a headless deployment, and this avoids leaving a licence file on shared runners.
  • Licensing messages go to stderr, never stdout, which belongs exclusively to the JSON protocol. If a session comes up unexpectedly limited, read the subprocess's stderr — it will say why.
  • A rejected key never revokes a working activation. If the machine already had a valid stored licence and the variable is wrong, the session keeps its entitled status and logs a warning, rather than dropping to Unlicensed because of a typo.

If neither source yields a valid licence the session still starts — the UI/RPC shell is available — but import and calculate raise with "An active SurfCalc license or trial is required."

session.run_surface_deformation(solver_type, filepath, **kwargs)

Reproduces the GUI's surface deformation fit: loads a surface (CSV or FEA result), rotates it into the local surface-normal frame, optionally masks a hole, optionally subtracts rigid body motion (RBM), then runs the selected fit method. Requires an active license or trial (same entitlement gate as the GUI).

Parameters:

Name Default Meaning
solver_type (required) "csv", "abaqus", "ansys", "nastran", "zemax_grid_sag" (Zemax .dat / .sag), or "codev_interferogram" (CODE V .int)
filepath (required) Path to the displacement/result file
grid_companion_dat, grid_diameter None, None Optical grid formats only. A CODE V .int header records no spatial extent, so one of these is required for it: the path to the .dat exported alongside it (exact), or the grid's clear-aperture width in units_disp_in, assumed centred on the origin. Raises ValueError if neither is given. Ignored for "zemax_grid_sag", whose header carries its own geometry. Unused Grid Sag cells (sag magnitude ≥ 1e10, OpticStudio's marker) are dropped on import
node_set None FEA node-set name (FEA inputs only); auto-picks "Default (All nodes)" or the first available set if omitted
step, frame_idx None, 0 FEA step/frame selection (FEA inputs only); auto-picks the last step if omitted
fit_method "circular_zernike" One of "circular_zernike", "annular_zernike", "chebyshev", "hexagonal", "bending_modes"
zernike_selection "1-37" Term index range/list (circular/annular/hexagonal fits)
zernike_type "Noll Zernike" Zernike ordering/normalization convention
value_mode "coefficient" "coefficient" (signed) or "magnitude" (absolute value) for reported term values
units_disp_in "mm" Input coordinate/displacement unit, converted internally to mm
normal_type "Compute from best fit plane" Or "Custom Vector" with custom_normal=(nx, ny, nz)
mask_hole, hole_diam False, 0.0 Optional central-hole masking. Requires hole_diam > 0 in units_disp_in (no silent auto-detect — use detect_hole_diameter_from_file / the GUI Auto button first). All surface nodes are kept; bridging triangles are excluded from area/normals. (hole_points / hole_method / hole_boundary_* remain accepted for back-compat and are ignored.)
subtract_rbm True Subtract best-fit rigid body motion before fitting
deform_quantity "nd" "nd" (normal displacement \(u\cdot\hat{n}\), default) or "uz" (local-Z sag). Drives the fit, RMS/PV, coefficients, and any later Grid Sag / CODE V export of that result
radial_correction "none" "none" (default, unchanged behavior), "linear", or "nonlinear". On a powered (curved) surface, a node's own in-plane motion changes its raw axial displacement even with no real optical error — it's sliding along the nominal prescription. Radial correction removes that geometric component before fitting; a flat surface is unaffected either way. See .agents/theory_pills/radial_correction.html in the source repo for the derivation
surface_roc, surface_conic 0.0, 0.0 Vertex radius of curvature (in units_disp_in) and conic constant of the nominal prescription. Ignored when radial_correction == "none"
annular_epsilon 0.35 Annular Zernike inner/outer radius ratio (annular fit only)
cheby_width, cheby_height 1.2, 0.8 Rectangular aperture dimensions (Chebyshev fit only)
hex_flat, hex_auto_detect 1.0, True Flat-to-flat size / auto-detect aperture (hexagonal fit only)
bending_csv, bending_use_all, bending_n_modes None, True, None Mode-shape CSV and mode-count selection (bending-modes fit only)

[!NOTE] surface_roc's declared unit is genuinely different per function — check the table for whichever one you're calling before assuming it matches another: * run_surface_deformationunits_disp_in * run_active_opticstarget_units_coords_in * run_influence_matrixunits_coords_in * run_load_case_sweepSI meters, always (no unit parameter at all)

surface_conic is always dimensionless.

Returns a dict (shape varies slightly by fit_method, but always includes):

  • coefficients — list of {idx, name, value} per fitted term
  • z_fit, z_residual — fitted and residual scalar surfaces (as plain lists)
  • metrics_fit, metrics_residual — each {pv, rms, mean, std}
  • rbm{tx, ty, tz, rx, ry, rz} fitted rigid body motion
  • deform_quantity"nd" or "uz", matching the argument above
  • radial_correction"none", "linear", or "nonlinear", matching the argument above; when not "none", surface_roc and surface_conic are also echoed back
result = session.run_surface_deformation(
    "csv", "displacements.csv",
    fit_method="circular_zernike",
    zernike_selection="1-15",
    subtract_rbm=True,
)

print(f"Fit RMS: {result['metrics_fit']['rms']}")
print(f"Residual RMS: {result['metrics_residual']['rms']}")
print(f"Fitted RBM: {result['rbm']}")

The input CSV must contain X, Y, Z, UX, UY, UZ columns (optionally NX, NY, NZ, Area).

session.detect_hole_diameter(x, y, *, area=None, units_disp_in="mm") / detect_hole_diameter_from_file(...)

Headless twin of the GUI Mask Inner Hole → Auto button. Returns {"diameter", "radius_m", "center", "units_disp_in"} where diameter is in units_disp_in (0.0 when no central hole is found). Pass that diameter into run_surface_deformation(..., mask_hole=True, hole_diam=...) or run_load_case_combination(..., mask_hole=True, hole_diam=...).

hole = session.detect_hole_diameter_from_file("csv", "displacements.csv", units_disp_in="mm")
result = session.run_surface_deformation(
    "csv", "displacements.csv",
    units_disp_in="mm",
    mask_hole=hole["diameter"] > 0,
    hole_diam=hole["diameter"],
)

session.run_load_case_combination(cases, mode="superpose", output_path=None, **kwargs)

Combines several load cases into one field — data preparation, not fitting — the headless counterpart of the Load Case Combination module. Fitting the combined result is a separate run_surface_deformation() call (see below); combination and fitting are independent steps, matching the app's own module split.

cases is a list of dicts; only solver_type and filepath are required:

Key Meaning
solver_type, filepath As for run_surface_deformation.
scale Signed multiplier (default 1.0).
name Label used in the returned provenance (defaults to the file's basename).
enabled Set False to skip a case (default True).
node_set, step, frame_idx FEA selection, as for run_surface_deformation.
units_disp_in Per-case input units; falls back to the call's units_disp_in.

mode is "superpose" (signed vector sum — a realizable deformation state) or "rss" (root-sum-square envelope of the normal displacements — a statistical bound, not a realizable state).

Cases need not share a mesh: the most refined mesh becomes the reference, and every other case is recentred on its own area-weighted centroid and interpolated onto it. The result is a _data-shaped dict (x/y/z/ux/uy/uz/nx/ny/nz/area) plus a provenance key recording the mode, the reference case, and each case's scale, node count, and whether it was interpolated. Pass output_path to also write it as a re-importable CSV (in SI metres).

mask_hole / hole_diam mirror the module's Mask Inner Hole control. hole_diam must be > 0 in units_disp_in (no silent auto-detect; the GUI Auto button fills that field first). Surface nodes are never deleted — only bridging triangles are excluded from area/normals. (hole_points / hole_method / hole_boundary_xyz remain accepted for back-compat and are ignored.)

combined = session.run_load_case_combination(
    [
        {"solver_type": "csv", "filepath": "gravity.csv", "scale": 1.0, "name": "gravity"},
        {"solver_type": "csv", "filepath": "thermal.csv", "scale": 0.6, "name": "thermal"},
    ],
    mode="superpose",
    output_path="combined.csv",
)
print(combined["provenance"])

result = session.run_surface_deformation("csv", "combined.csv", units_disp_in="m", zernike_selection="1-15")
print(f"Residual RMS: {result['metrics_residual']['rms']}")

Load Case Combination requires an active license or trial; the gate is enforced identically here and in the GUI. license_manager is resolved from the process's own licence state and is ignored if sent by a client.

SurfCalcAPI.run_load_case_sweep(cases, *, variables, swept_vars, **kwargs)

The headless counterpart of the sidebar's Grid Sweep (from SurfCalcAPI import run_load_case_sweep — reachable over the RPC protocol's raw JSON dispatch too, but not yet wrapped by SurfCalcSession, so it is not available as session.run_load_case_sweep(...) through the shipped client).

cases is shaped like run_load_case_combination's. variables is the full name → value map (fixed values for anything not swept); swept_vars is a list of {"name", "min", "max", "step"} — the Cartesian product is the grid. Every grid point reports combined_rms and combined_pv of the combined field's normal displacement (area-weighted, the same convention Surface Deformation uses for fit residuals). Rows are sorted worst-first by combined_rms; display-side ranking is free to re-sort by any other column.

evaluate ("combined" default, "surface_fit", "active_optics") adds a further analysis step at each grid point — load magnitude and combination are the genuine unknown in a STOP analysis, so this is how a grid point gets judged by what survives a fit or a correction rather than by the raw mechanical deformation:

evaluate Extra kwargs Row keys added
"surface_fit" fit_kwargs — any run_surface_deformation fit parameter (fit_method, zernike_selection, ...) fit_residual_rms, fit_residual_pv, fit_rms, fit_pv
"active_optics" ao_inf_filepath, ao_kwargs (enable_limits, fmin, fmax, alpha, enable_regularization, reg_type, remove_rbm, mask_hole, hole_diam) ao_residual_rms, ao_residual_pv, correction_factor, afi, ao_max_command, ao_over_budget

ao_over_budget is True when at least one actuator command saturates at its configured fmin/fmax — the honest signal that this load case needs more than the actuator can give, since a constrained solve otherwise clips silently and a bare force number would not show the shortfall.

radial_correction, surface_roc, surface_conic ("none", 0.0, 0.0) — see run_surface_deformation's radial_correction entry above for what this does. Applied to the combined field's own normal displacement before either metric is computed, so it affects combined_rms/combined_pv and (for the "active_optics" evaluate stage only — see the unit-convention note below) the corrected result. surface_roc here is SI METERS, not units_disp_in — the combined field this operates on is always metric internally, and there is no separate coordinate-unit parameter on this function to declare it against.

Every grid point's own mesh geometry is identical (only the scale factors, and so ux/uy/uz, vary), so the "active_optics" stage's rigid registration onto the influence matrix mesh runs once for the whole sweep, not once per point.

from SurfCalcAPI import run_load_case_sweep

# Which gravity-angle case needs the biggest actuator command?
result = run_load_case_sweep(
    [{"solver_type": "csv", "filepath": "gravity_case.csv", "scale": 1.0, "scale_expr": "g"}],
    variables={"g": 1.0}, units_disp_in="m",
    swept_vars=[{"name": "g", "min": 0.5, "max": 1.5, "step": 0.25}],
    evaluate="active_optics", ao_inf_filepath="influence_matrix.csv",
    ao_kwargs={"enable_limits": True, "fmin": -0.5, "fmax": 0.5},
)
print(result["rows"][0])  # worst case first

session.run_influence_matrix(checked_entries, optical_set, **kwargs)

Reproduces the GUI's influence matrix compile step: compiles an influence matrix from checked FEA result "steps" and requires entitlement, exactly as the GUI does.

Parameters:

Name Default Meaning
checked_entries (required) List of entries as returned by list_result_steps (one-file-per-actuator, a multi-step results file, or CSV columns)
optical_set (required) Node-set name to load from each FE results file (ignored for CSV entries)
remove_rbm False Subtract best-fit RBM before projecting displacement onto the surface normal
symmetry_type None "Rotational" (with num_sectors) or "Bilateral" (with plane_angle_deg) — when given, checked_entries are treated as the master actuators and expanded via symmetry
num_sectors, plane_angle_deg None, None Symmetry parameters, required only for the matching symmetry_type
radial_correction, surface_roc, surface_conic "none", 0.0, 0.0 See run_surface_deformation's radial_correction entry above. Influence Matrix has no per-actuator local aperture frame the way Surface Deformation does, so this uses each actuator's own mesh recentred at its own area-weighted centroid as the assumed aperture centre. surface_roc must be in units_coords_in — this compile runs directly in the results file's own raw coordinate unit, so the value is used as-is, never converted. Ignored (forced to "none") for CSV checked_entries, which carry an already-scalar column with no vector to correct
units_coords_in "mm" Documents which unit surface_roc is expressed in; must match the results file's own raw coordinate unit — see above

Returns a dict shaped like the influence-matrix CSV loader's output: inf_matrix, x, y, z, actuator_ids, n_actuators, n_points. When radial_correction != "none", also includes radial_correction, surface_roc, surface_conic, units_coords_in matching the arguments above.

Raises if the mesh/actuator counts exceed the active tier's limits (see Software Licensing).

session.run_active_optics(inf_filepath, target_filepath, **kwargs)

Reproduces the GUI's active optics correction: loads the influence matrix and target, requires entitlement, registers the target onto the influence matrix's mesh frame and resamples it there (raising on incompatible meshes — there's no interactive override in headless mode), then solves.

Parameters:

Name Default Meaning
inf_filepath (required) Path to the compiled influence matrix file
target_filepath (required) Path to the target deformation file (CSV, FEA, or an optical grid file)
target_solver_type None "csv", "abaqus", "ansys", "nastran", "zemax_grid_sag", or "codev_interferogram". Auto-detected from the file if omitted
grid_companion_dat, grid_diameter None, None As for run_surface_deformation — required for a "codev_interferogram" target, which carries no spatial extent of its own
node_set, step, frame_idx None FEA target selection (FEA inputs only)
target_units_coords_in, target_units_disp_in "mm", "mm" Target file unit conversion (FEA inputs only)
enable_limits, fmin, fmax False, 0.0, 0.0 Bounded-solver actuator force limits
remove_rbm False Subtract RBM from the target before solving
alpha, enable_regularization, reg_type 0.0, False, "Identity" Tikhonov regularization strength and type ("Identity" or "Laplacian Smoothing")
target_csys None Optional {"origin": (x,y,z), "x_axis": (x,y,z), "xy_plane": (x,y,z)} — the target file's own local CSYS, in the same 3-point convention the Influence Matrix module exports. When omitted (or the influence-matrix file has no local CSYS), registration falls back to translation-only alignment
radial_correction, surface_roc, surface_conic "none", 0.0, 0.0 See run_surface_deformation's radial_correction entry above. Only the target is corrected — Active Optics' influence matrix is already a scalar (Actuator_N columns are pre-projected, with no displacement vector to correct). surface_roc is in target_units_coords_in. Raises ValueError (no interactive fallback in headless mode) when the target has no displacement vector to correct — a legacy scalar-only CSV, or a Grid Sag / CODE V interferogram target

Returns a dict with:

  • forces — actuator forces
  • z_fit, z_residual — corrected surface and residual
  • radial_correction — matching the argument above; when not "none", surface_roc and surface_conic are also echoed back
  • rms_original, pv_original, rms_residual, pv_residual — before/after surface metrics
  • correction_factor — % reduction in RMS
  • afi — Actuator Fighting Index
  • rbm — fitted RBM dict or None
  • mesh_compatibility, alignment — diagnostics from the target-registration step

session.run_bending_modes_extract(filepath, **kwargs)

Headless twin of Bending Modes Extract Modes (including the FEA load-all-modes step the GUI exposes as Load & Preview Set).

Parameters:

Name Default Meaning
filepath (required) Modes CSV or FEA modal database
solver_type "csv" "csv", "abaqus", "nastran", or "ansys"
n_modes None Keep the first N modes (None = all)
units_coords_in "mm" Length unit for coordinates and mode amplitudes
mode_normalization "none" "none", "peak", or "rms"
node_set, step "Default (All nodes)", None FEA selection (step=None → last step)
custom_normal None Optional (nx, ny, nz) for CSV projection
output_path None Optional path to also write a SurfCalc modes CSV (SI metres)

Returns a dict with x, y, z, nx, ny, nz, area, modes, n_modes_extracted, mode_normalization (and mode_labels when the FEA importer provides them).

session.run_sensitivity_matrix(rows, **kwargs)

Headless twin of the Sensitivity Matrix tab's Compute Sensitivity Matrix: builds a real first-order (paraxial) rigid-body tilt/decenter → line-of-sight sensitivity matrix from an ordered optical prescription. First-order/paraxial only — no aberration, no despace/clocking sensitivity (both are zero at this order for a rotationally symmetric surface), object at infinity, unfolded train.

Parameters:

Name Default Meaning
rows (required) List of prescription-row dicts, one per element — {"name", "element_type" ("mirror"/"lens"/"image"), "axial_position_m", "power_mode" ("roc"/"focal"/"mirror_id"), "roc_m", "focal_m", "mirror_id", "n_after", "fea_vertex_z_m", "is_stop", "notes"}. The last row must be "image"; axial_position_m must strictly increase. A "mirror_id" row is resolved against the Optical Surface Library at compute time
output "image_plane" "image_plane" (metres of image-plane motion per metre of decenter / per radian of tilt) or "sky_angle" (radians of sky angle per unit input) — the latter raises on an afocal train

Returns a dict with matrix, output_labels (["LOS_X", "LOS_Y"]), surface_labels ("Name:Dof" columns, Dof one of Tx/Ty/Rx/Ry), surface_names, dof_labels, b_coefficients_m, efl_m, back_focal_distance_m, output_units, and prescription (the resolved rows).

session.load_optical_prescription(filepath, *, fmt=None)

Headless twin of the Sensitivity Matrix tab's Import from Zemax/CODE V: parses a real Zemax OpticStudio (.zmx) or CODE V (.seq/.len) optical prescription into rows directly passable to run_sensitivity_matrix(). Not licence-gated — a pure parse, same trust boundary as loading a prescription CSV by hand.

Parameters:

Name Default Meaning
filepath (required) Path to a .zmx, .seq, or .len file
fmt None "zemax_zmx" or "codev_seq" to skip auto-detection; None decides by extension, falling back to content-sniffing

Returns (rows, warnings). rows are run_sensitivity_matrix()-ready dicts; check each row's "needs_review" before computing — set when an imported field couldn't be fully resolved (currently: a refractive element, whose focal length isn't derivable from the prescription alone). warnings are human-readable notes about anything reduced during import (e.g. how many mirror-reflection segments were unfolded). Raises ValueError for a non-sequential/NSC file, a coordinate-break/decenter card, or a finite object distance — SurfCalc's paraxial engine supports on-axis, sequential, object-at-infinity trains only.

session.run_image_motion(surfaces, sensitivity_matrix_filepath)

Headless twin of the Image Motion tab's Calculate LOS Budget: combines each surface's real deformation with a sensitivity matrix (normally run_sensitivity_matrix()'s output, exported to CSV) into a line-of-sight error budget. Three input shapes are auto-detected from the matrix's own row/column labels: the recommended run_sensitivity_matrix() output ("rigid_body", 2 x 4·n_surfaces, columns Name:Tx/Name:Ty/Name:Rx/Name:Ry); a 6-row per-mirror matrix ("external_6dof", 6 x n_surfaces, rows labeled with the 6 rigid-body DOFs Tx/Ty/Tz/Rx/Ry/Rz or a recognized synonym) as typically exported by a full ray-traced optical model (Zemax/CODE V perturbation analysis) rather than this app's own first-order engine — unlike "rigid_body", this path also reads Tz/Rz (piston/clocking), which a real model can have genuine sensitivity to; and a legacy hand-authored (LOS_X/LOS_Y) x (bare surface name) matrix ("legacy_inplane_rms"), which falls back to reading raw UX/UY RMS for backward compatibility and cannot see a pure rigid-body tilt.

Parameters:

Name Default Meaning
surfaces (required) List of (filepath, fea_vertex_z_m) tuples, one per surface, in the SAME ORDER as the sensitivity matrix's own surfaces. filepath=None (or a missing file) leaves that surface's contribution at zero
sensitivity_matrix_filepath (required) Path to the sensitivity matrix CSV

Returns a dict with mode ("rigid_body", "external_6dof", or "legacy_inplane_rms"), contributions (per-surface name, rms_x/rms_y/pv_x/pv_y, signed_x/signed_y, rbm), total_rms_x/total_rms_y/total_rms (RSS across surfaces — a statistical tolerance-budget figure), and total_signed_x/total_signed_y/total_signed (the algebraic sum — the deterministic answer for one FEA load case; None in "legacy_inplane_rms" mode). "external_6dof" has only one output axis (the row axis already IS the 6 DOF, not an output pair), so its rms_y/signed_y/total_rms_y/total_signed_y are always 0.0/None/0.0/None — the real number is carried in the _x fields.

Console / GUI-replay form:

session.run_random_response_budget(mode_basis_filepath, modal_sigma, **kwargs)

Headless twin of Random Response Budget's Compute Budget (Modal Random-Response Zernike Budget — see docs/THEORY_MANUAL.md §17 for the math). Unlike a Prepare-stage extraction call, this module performs no mode extraction of its own: mode_basis_filepath is a SurfCalc mode-basis CSV (export_modes_csv's format — typically Bending Modes' own Export Modes CSV output, or run_bending_modes_extract(..., output_path=...)'s). It fits Zernikes to each mode in that basis individually and RSS-combines them using the caller-supplied per-mode σ. SurfCalc never derives σ itself — see the User Guide's Random Response Budget Module for how to obtain it from Nastran/Abaqus/Ansys.

Parameters:

Name Default Meaning
mode_basis_filepath (required) Path to a mode-basis CSV (export_modes_csv's format)
modal_sigma (required) list[float], length must equal the basis's mode count, already in SI metres — no unit conversion is performed here
selection_str "1-37" Zernike term selection
zernike_type "Noll Zernike" "Noll Zernike" or "Fringe Zernike"
n_realizations 2000 Monte Carlo realization count for peak-to-valley statistics
seed None Optional int for reproducible Monte Carlo results

Returns a dict with coefficients (per-term 1-sigma budget), rms_total, rms_captured, rms_residual, orthogonality_closure, envelope_field, monte_carlo (pv_median, pv_p95, pv_over_rms_total, worst_case_shape, seed_used), n_modes_used, and mode_normalization (echoed from the basis, None if it declares none).

Console / GUI-replay form:

surfcalc.compute_random_response_budget()  # <instance label>

session.write_surface_deformation_report(result, filepath, **kwargs)

Render a run_surface_deformation() result as a self-contained HTML report — the GUI-free twin of the Surface Deformation tab's Analysis Report (.html) export, using the same renderer (core/report.py) so both writers lay out fit setup, metrics, RBM and coefficients identically. Returns the path written.

Two differences from the GUI export. This function receives only the numeric result dict, not a live 3D viewport, so the report it writes has no Original/Fit/Residual pane images — those only exist in the GUI export, which screenshots the actual on-screen plotters. And it never opens a browser: a script running unattended (a batch job, --serve-stdio) must not pop a window on whatever machine happens to be running it, so launching the file (or not) is left to the caller. The SurfCalc-branded header and everything else match.

Argument Default Meaning
result The dict returned by run_surface_deformation(). Its coefficients and metrics are raw SI metres.
filepath Where to write the .html file.
units "nm" Display unit for coefficients and translations. "waves" is not accepted here — it needs a reference wavelength that only the GUI carries — so pass an absolute unit such as "nm" or "um". Rotations are always reported in radians.
fit_method "circular_zernike" Recorded in the Fit Setup block; pass whatever you gave run_surface_deformation().
zernike_type "Noll Zernike" Recorded for Zernike-family fits.
title "Surface Deformation" Report heading.
source "" Free-form provenance line (source file, upstream instance) shown in the header.
res = session.run_surface_deformation("abaqus", "M1_gravity.odb", node_set="OPTICAL")
session.write_surface_deformation_report(res, "M1_gravity_report.html",
                                         units="nm", source="M1_gravity.odb (Abaqus)")

The output is a single file with its CSS inlined, so it survives being mailed on its own. SurfCalc bundles no PDF engine by design — a browser's Print to PDF is the supported route to a PDF deliverable, and the report carries print stylesheet rules for it.

session.write_active_optics_report(result, filepath, **kwargs)

Render a run_active_optics() result as a self-contained HTML report — the GUI-free twin of the Active Optics tab's Analysis Report (.html) export, using the same renderer (core/report.py) as write_surface_deformation_report(). Returns the path written.

Same two differences from the GUI export as the Surface Deformation writer: no Target/Fit/Residual/Overlay pane images (this function has no live viewport to screenshot), and it never opens a browser (a script running unattended must not pop a window on whatever machine happens to be running it).

Argument Default Meaning
result The dict returned by run_active_optics(). Its RMS/PV figures are raw SI metres; forces is raw SI (newtons, or metres if the influence matrix was built from displacement pokes).
filepath Where to write the .html file.
units "nm" Display unit for the RMS/PV metrics. "waves" is not accepted here, same reason as the Surface Deformation writer.
actuator_type "force" "force" or "displacement" — picks which unit table actuator_unit is resolved against.
actuator_unit None Display unit for the actuator table. Defaults to "N" for force, "nm" for displacement.
title "Active Optics" Report heading.
source "" Free-form provenance line (source file, upstream instance) shown in the header.
res = session.run_active_optics("inf_matrix.csv", "target.odb", target_solver_type="abaqus")
session.write_active_optics_report(res, "M1_correction_report.html",
                                   units="nm", actuator_type="force", actuator_unit="N")

surfcalc.switch_tab("bending_modes")
surfcalc.import_file("csv", "modes.csv", target="bending_modes", units_coords="m")
surfcalc.calculate()  # Extract Modes
# FEA path: calculate() auto-loads a keyword-matched optical node set.
# Call load_bm_preview_set() first only when the selected set is Default
# (All nodes) or otherwise unmatched.
# surfcalc.load_bm_preview_set()
# surfcalc.calculate()

3. Macro Recording

Macro > Record Macro... captures GUI actions as a replayable Python script calling into GUISurfCalcAPI, that you can re-run later via Macro > Run Script... or the Engineering Console. See Macro Recording & Automation Script Export in the User Guide for how to record, edit, and re-run one.