dragonfly_sim.utils.dv_sensitivity

Total-derivative norms per individual design parameter.

Motivation

PODBasis.construct_basis’s local_weighted mode weights snapshots by a plain Euclidean distance across the snapshot parameter vector. When design variables of different physical character share that vector – control point motions in metres alongside angle of attack in radians – an equal distance in different dimensions implies a wildly different change in the state, so the weighting ends up dominated by whichever variable happens to have the loosest bound. The intended fix is a diagonal weighting matrix on the distance, one weight per design parameter, sized by how strongly the solution responds to that parameter.

Adjoint-based optimization never forms du/dx, so the stand-in used here is the total derivative of the OUTPUT quantities, dF/dx, which the gradient evaluation already computes. This module extracts it and reports, for each individual design parameter j,

s_j = || [dF_1/dp_j, …, dF_m/dp_j] ||_2

over all optimization outputs F (objective and constraints).

Where the numbers come from

SimulatorBase.compute_optimization_derivatives() returns df of shape (1, n_dv) and dc of shape (n_c, n_dv) – the total derivatives of the objective and constraints with respect to the flattened design variable vector, in PHYSICAL (unscaled) design variable units. modopt applies its own o_scaler/x_scaler only afterwards, so what the simulator hands back is exactly what is wanted here. attach_total_derivative_probe wraps that method on the simulator INSTANCE, so nothing in csdl_alpha or modopt is modified and no additional solves are performed – it reads the dict that is already being returned to the optimizer.

sim.dv_meta / sim.c_meta / sim.o_meta (built by csdl_alpha.backends.simulator.build_opt_metadata) map each csdl Variable to its l_ind:u_ind slice of the flat vector, which is what lets every column be labelled with its design variable and its index inside that variable.

Units: param_scales

If the POD parameter vector is not the design variable vector verbatim, param_scales[j] is the factor mapping one to the other, p_j = param_scales[j] * x_j, so the reported sensitivities are divided by it:

dF/dp_j = (dF/dx_j) / param_scales[j]

That puts the printed numbers directly in the units the POD distance is measured in. In DRAGonFLy the snapshot parameter vector (DG_windtunnel_model. _parameter_vector: the flat shape parameters, then alpha) is the design variable vector, so param_scales is normally left out.

Dependencies

Deliberately numpy-only in its numeric core – no csdl, petsc or dolfinx – following the same rule as ffd_dv_utils, so it can be exercised without an MPI environment. extract_metadata is the single function that touches csdl objects, and it only reads plain attributes (.name, .shape, .size). Callers pass their own printer (typically PETSc.Sys.Print) to keep output on rank 0.

Classes

DVSensitivityMeta

Labels for the rows and columns of the stacked total-derivative matrix.

Functions

_entry_labels(name, shape, size)

One label per entry of a variable, flattened in C order.

_stack_output_values(deriv_dict, n_out)

The objective and constraint VALUES, in the row order of the stacked J.

attach_total_derivative_probe(sim[, printer, ...])

Print the per-parameter total-derivative norms on every gradient evaluation.

extract_metadata(sim)

Build a DVSensitivityMeta from a csdl_alpha simulator.

format_total_derivative_norms(norms, meta[, header, ...])

Render the norms as a printable multi-line table.

print_total_derivative_norms(deriv_dict, meta[, ...])

Compute and print the norms for one compute_optimization_derivatives result.

stack_total_derivatives(df, dc)

Stack the objective gradient and constraint Jacobian into one (n_out, n_dv).

total_derivative_norms(J[, meta, param_scales, ...])

Column and block norms of the stacked total-derivative matrix.

Module Contents

class dragonfly_sim.utils.dv_sensitivity.DVSensitivityMeta

Bases: NamedTuple

Labels for the rows and columns of the stacked total-derivative matrix.

param_labelsn_dv strings, one per individual design parameter, in the

order of the flattened design variable vector

group_slices : [(name, l_ind, u_ind)] per design VARIABLE, same order output_labels: n_out strings, objective first then constraints, matching the

row order of stack_total_derivatives

group_slices: list
property n_dv
property n_out
output_labels: list
param_labels: list
dragonfly_sim.utils.dv_sensitivity._entry_labels(name, shape, size)

One label per entry of a variable, flattened in C order.

C order is not a free choice: build_opt_metadata fills the flat vector with var.value.flatten(), and ffd_dv_utils.build_cp_motion_dv builds the matching bounds the same way, so entry [i, j, k] of a design variable is at flat index ravel_multi_index((i, j, k), shape).

dragonfly_sim.utils.dv_sensitivity._stack_output_values(deriv_dict, n_out)

The objective and constraint VALUES, in the row order of the stacked J.

Returns None if they are absent or do not add up to n_out, in which case the magnitude-normalized column norms are simply skipped – they are a diagnostic, not worth failing the probe over.

dragonfly_sim.utils.dv_sensitivity.attach_total_derivative_probe(sim, printer=print, param_scales=None, record_to=None, record_attr='total_derivative_norms_per_iteration', eval_idx_getter=None, on_norms=None, max_entries=64)

Print the per-parameter total-derivative norms on every gradient evaluation.

Wraps compute_optimization_derivatives on the simulator INSTANCE (an instance attribute shadowing the class method), which is what keeps this out of csdl_alpha and modopt entirely. modopt guards that call with its own warm-x check, so it fires once per new design point and costs no additional solves – the wrapper only reads the dict already being returned.

Parameters:
sima csdl_alpha SimulatorBase set up as an optimization problem
printercallable taking one string; pass PETSc.Sys.Print under MPI. The

derivatives are replicated across ranks (the custom operations allreduce their jacvec products), so rank-0 printing loses nothing.

param_scales(n_dv,) design-variable-to-parameter unit conversion; see the

module docstring. Its length is checked against the design variable count HERE, at attach time, rather than at the first gradient evaluation – that is the guard that catches a design variable being added without the snapshot parameter vector being updated together.

record_tooptional object (a DataStore) with a list attribute named

record_attr, appended with (eval_idx, column_norms) per call.

eval_idx_gettercallable returning the evaluation index these derivatives

belong to. Without one, the probe counts its own calls.

on_normsoptional callable receiving the full norms dict after each

gradient evaluation. This is how a consumer that wants to ACT on the sensitivities gets them – see ReducedOrderModel.set_distance_sensitivities, which turns them into the diagonal weights for the local_weighted POD distance. Called inside the same guard as the reporting, so a consumer that raises degrades the diagnostic rather than the optimization.

Returns:
callable that detaches the probe.
dragonfly_sim.utils.dv_sensitivity.extract_metadata(sim)

Build a DVSensitivityMeta from a csdl_alpha simulator.

The ONLY function here that touches csdl objects. Everything downstream works on numpy arrays and plain python lists.

Variables created by CustomOperation.create_output carry no name (it does not call add_name), so unnamed outputs fall back to positional labels. Give the objective a name with add_name at the call site if the printout should identify it.

dragonfly_sim.utils.dv_sensitivity.format_total_derivative_norms(norms, meta, header=None, max_entries=64)

Render the norms as a printable multi-line table.

The per-group summary and the per-output x per-group block norms are always shown; the full per-parameter listing only when there are at most max_entries design parameters, since a large FFD block would otherwise bury the rest of the log.

dragonfly_sim.utils.dv_sensitivity.print_total_derivative_norms(deriv_dict, meta, printer=print, param_scales=None, header=None, max_entries=64)

Compute and print the norms for one compute_optimization_derivatives result.

Returns the norms dict, or None when the result carried no derivatives at all.

dragonfly_sim.utils.dv_sensitivity.stack_total_derivatives(df, dc)

Stack the objective gradient and constraint Jacobian into one (n_out, n_dv).

df is (1, n_dv) and dc is (n_c, n_dv), exactly as compute_optimization_derivatives returns them; either may be None (an unconstrained problem, or a feasibility problem with no objective). Returns None when both are.

dragonfly_sim.utils.dv_sensitivity.total_derivative_norms(J, meta=None, param_scales=None, output_values=None)

Column and block norms of the stacked total-derivative matrix.

Parameters:
J(n_out, n_dv) array from stack_total_derivatives
metaDVSensitivityMeta, optional – needed for the labelled block norms
param_scales(n_dv,) array, optional. p_j = param_scales[j]*x_j; J is

divided by it so every reported quantity is per unit of the POD parameter, not per unit of the design variable. See the module docstring.

output_values(n_out,) array, optional – the values of the outputs

themselves, used for the magnitude-normalized variant.

Returns:
dict with keys ‘J’ (the scaled matrix), ‘column_norms’,
‘column_norms_relative’ (None without output_values) and ‘block_norms’
(None without meta).