dragonfly_sim.utils.dv_sensitivity ================================== .. py:module:: dragonfly_sim.utils.dv_sensitivity .. autoapi-nested-parse:: 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. .. !! processed by numpydoc !! Classes ------- .. autoapisummary:: dragonfly_sim.utils.dv_sensitivity.DVSensitivityMeta Functions --------- .. autoapisummary:: dragonfly_sim.utils.dv_sensitivity._entry_labels dragonfly_sim.utils.dv_sensitivity._stack_output_values dragonfly_sim.utils.dv_sensitivity.attach_total_derivative_probe dragonfly_sim.utils.dv_sensitivity.extract_metadata dragonfly_sim.utils.dv_sensitivity.format_total_derivative_norms dragonfly_sim.utils.dv_sensitivity.print_total_derivative_norms dragonfly_sim.utils.dv_sensitivity.stack_total_derivatives dragonfly_sim.utils.dv_sensitivity.total_derivative_norms Module Contents --------------- .. py:class:: DVSensitivityMeta Bases: :py:obj:`NamedTuple` Labels for the rows and columns of the stacked total-derivative matrix. param_labels : n_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`` .. !! processed by numpydoc !! .. py:attribute:: group_slices :type: list .. py:property:: n_dv .. py:property:: n_out .. py:attribute:: output_labels :type: list .. py:attribute:: param_labels :type: list .. py:function:: _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)``. .. !! processed by numpydoc !! .. py:function:: _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. .. !! processed by numpydoc !! .. py:function:: 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: **sim** : a csdl_alpha SimulatorBase set up as an optimization problem .. **printer** : callable 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_to** : optional object (a DataStore) with a list attribute named ``record_attr``, appended with ``(eval_idx, column_norms)`` per call. **eval_idx_getter** : callable returning the evaluation index these derivatives belong to. Without one, the probe counts its own calls. **on_norms** : optional 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. .. .. !! processed by numpydoc !! .. py:function:: 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. .. !! processed by numpydoc !! .. py:function:: 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. .. !! processed by numpydoc !! .. py:function:: 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. .. !! processed by numpydoc !! .. py:function:: 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. .. !! processed by numpydoc !! .. py:function:: 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 .. **meta** : DVSensitivityMeta, 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). .. .. !! processed by numpydoc !!