FastModeSensitivityAnalyser.java

/*
 * Copyright (c) 2025, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.powsybl.flow_decomposition.partitioners;

import com.powsybl.contingency.ContingencyContext;
import com.powsybl.flow_decomposition.AbstractSensitivityAnalyser;
import com.powsybl.flow_decomposition.FunctionVariableFactor;
import com.powsybl.flow_decomposition.NetworkUtil;
import com.powsybl.iidm.network.Branch;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.PhaseTapChanger;
import com.powsybl.iidm.network.PhaseTapChangerStep;
import com.powsybl.loadflow.LoadFlowParameters;
import com.powsybl.sensitivity.*;

import java.util.*;

import static com.powsybl.flow_decomposition.DecomposedFlow.PST_COLUMN_NAME;

/**
 * @author Sebastien Murgey {@literal <sebastien.murgey at rte-france.com>}
 */
public class FastModeSensitivityAnalyser extends AbstractSensitivityAnalyser {
    private final Network network;
    private final Set<Branch> xnecs;
    private final Set<String> flowParts;
    private final Map<String, Map<String, Double>> nodalInjectionPartitions;

    FastModeSensitivityAnalyser(LoadFlowParameters loadFlowParameters,
                                SensitivityAnalysis.Runner runner,
                                Network network,
                                Set<Branch> xnecs,
                                SparseMatrixWithIndexesTriplet nodalInjectionsMatrix) {
        super(loadFlowParameters, runner);
        this.network = network;
        this.xnecs = xnecs;
        this.flowParts = nodalInjectionsMatrix.colIndex.keySet();
        this.nodalInjectionPartitions = nodalInjectionsMatrix.toMap();
    }

    private static String getNegativeFlowPartName(String flowPart) {
        return "Negative " + flowPart;
    }

    private static String getPositiveFlowPartName(String flowPart) {
        return "Positive " + flowPart;
    }

    private static double respectFlowSignConvention(double ptdfValue, double referenceFlow) {
        return referenceFlow < 0 ? -ptdfValue : ptdfValue;
    }

    public Map<String, Map<String, Double>> run() {
        List<SensitivityVariableSet> sensitivityVariableSets = new ArrayList<>();
        List<FunctionVariableFactor> sensitivityFactors = new ArrayList<>();
        Map<String, Double> nodalInjectionsPartitionSumByFlowPart = new HashMap<>();
        for (String flowPart : flowParts) {
            String positiveFlowPartName = getPositiveFlowPartName(flowPart);
            double positiveFlowPartSum = nodalInjectionPartitions.values().stream().filter(stringDoubleMap -> stringDoubleMap.containsKey(flowPart) && stringDoubleMap.get(flowPart) > 0).mapToDouble(stringDoubleMap -> stringDoubleMap.get(flowPart)).sum();
            String negativeFlowPartName = getNegativeFlowPartName(flowPart);
            double negativeFlowPartSum = nodalInjectionPartitions.values().stream().filter(stringDoubleMap -> stringDoubleMap.containsKey(flowPart) && stringDoubleMap.get(flowPart) < 0).mapToDouble(stringDoubleMap -> stringDoubleMap.get(flowPart)).sum();
            sensitivityVariableSets.add(new SensitivityVariableSet(
                    positiveFlowPartName,
                    nodalInjectionPartitions.entrySet().stream().filter(entry -> entry.getValue().containsKey(flowPart) && entry.getValue().get(flowPart) > 0).map(entry -> new WeightedSensitivityVariable(entry.getKey(), entry.getValue().get(flowPart) / positiveFlowPartSum)).toList()));
            nodalInjectionsPartitionSumByFlowPart.put(positiveFlowPartName, positiveFlowPartSum);
            sensitivityVariableSets.add(new SensitivityVariableSet(
                    negativeFlowPartName,
                    nodalInjectionPartitions.entrySet().stream().filter(entry -> entry.getValue().containsKey(flowPart) && entry.getValue().get(flowPart) < 0).map(entry -> new WeightedSensitivityVariable(entry.getKey(), entry.getValue().get(flowPart) / negativeFlowPartSum)).toList()));
            nodalInjectionsPartitionSumByFlowPart.put(negativeFlowPartName, negativeFlowPartSum);
        }

        SensitivityFactorReader factorReader = new FastModeSensitivityFactorReader(flowParts, sensitivityFactors);
        Map<String, Map<String, Double>> results = new HashMap<>();
        SensitivityResultWriter valueWriter = new FastModeSensitivityResultWriter(sensitivityFactors, results, flowParts, nodalInjectionsPartitionSumByFlowPart);
        runSensitivityAnalysis(network, factorReader, valueWriter, sensitivityVariableSets);
        return results;
    }

    private class FastModeSensitivityResultWriter implements SensitivityResultWriter {

        private final List<FunctionVariableFactor> factors;
        private final Map<String, Map<String, Double>> results;
        private final Set<String> flowParts;
        private final Map<String, Double> nodalInjectionsPartitionSumByFlowPart;

        public FastModeSensitivityResultWriter(List<FunctionVariableFactor> factors, Map<String, Map<String, Double>> results, Set<String> flowParts, Map<String, Double> nodalInjectionsPartitionSumByFlowPart) {
            this.factors = factors;
            this.results = results;
            this.flowParts = flowParts;
            this.nodalInjectionsPartitionSumByFlowPart = nodalInjectionsPartitionSumByFlowPart;
        }

        @Override
        public void writeSensitivityValue(int factorIndex, int contingencyIndex, double value, double functionReference) {
            if (Double.isNaN(value)) {
                return;
            }
            FunctionVariableFactor factor = factors.get(factorIndex);
            Map<String, Double> flowDecomposition = results.computeIfAbsent(factor.functionId(), s -> new HashMap<>());
            for (String flowPart : flowParts) {
                if (factor.variableId().equals(getPositiveFlowPartName(flowPart))) {
                    double partialFlowPartValue = flowDecomposition.getOrDefault(flowPart, 0.0);
                    flowDecomposition.put(flowPart, partialFlowPartValue + respectFlowSignConvention(value * nodalInjectionsPartitionSumByFlowPart.get(getPositiveFlowPartName(flowPart)), functionReference));
                    return;
                } else if (factor.variableId().equals(getNegativeFlowPartName(flowPart))) {
                    double partialFlowPartValue = flowDecomposition.getOrDefault(flowPart, 0.0);
                    flowDecomposition.put(flowPart, partialFlowPartValue + respectFlowSignConvention(value * nodalInjectionsPartitionSumByFlowPart.get(getNegativeFlowPartName(flowPart)), functionReference));
                    return;
                }
            }

            PhaseTapChanger phaseTapChanger = network.getTwoWindingsTransformer(factor.variableId()).getPhaseTapChanger();
            Optional<PhaseTapChangerStep> neutralStep = phaseTapChanger.getNeutralStep();
            double deltaTap = 0.0;
            if (neutralStep.isPresent()) {
                deltaTap = phaseTapChanger.getCurrentStep().getAlpha() - neutralStep.get().getAlpha();
            }
            double pstFlow = flowDecomposition.getOrDefault(PST_COLUMN_NAME, 0.0);
            flowDecomposition.put(PST_COLUMN_NAME, pstFlow + respectFlowSignConvention(deltaTap * value, functionReference));
        }

        @Override
        public void writeContingencyStatus(int contingencyIndex, SensitivityAnalysisResult.Status status) {
            // We do not manage contingency yet
        }
    }

    private class FastModeSensitivityFactorReader implements SensitivityFactorReader {
        private final Set<String> flowParts;
        private final List<FunctionVariableFactor> factors;

        public FastModeSensitivityFactorReader(Set<String> flowParts, List<FunctionVariableFactor> factors) {
            this.flowParts = flowParts;
            this.factors = factors;
        }

        @Override
        public void read(Handler handler) {
            for (Branch xnec : xnecs) {
                for (String flowPart : flowParts) {
                    factors.add(new FunctionVariableFactor(xnec.getId(), getPositiveFlowPartName(flowPart)));
                    handler.onFactor(SENSITIVITY_FUNCTION_TYPE, xnec.getId(), SensitivityVariableType.INJECTION_ACTIVE_POWER, getPositiveFlowPartName(flowPart), true, ContingencyContext.none());
                    factors.add(new FunctionVariableFactor(xnec.getId(), getNegativeFlowPartName(flowPart)));
                    handler.onFactor(SENSITIVITY_FUNCTION_TYPE, xnec.getId(), SensitivityVariableType.INJECTION_ACTIVE_POWER, getNegativeFlowPartName(flowPart), true, ContingencyContext.none());
                }

                for (String pst : NetworkUtil.getPstIdList(network)) {
                    factors.add(new FunctionVariableFactor(xnec.getId(), pst));
                    handler.onFactor(SENSITIVITY_FUNCTION_TYPE, xnec.getId(), SensitivityVariableType.TRANSFORMER_PHASE, pst, false, ContingencyContext.none());
                }
            }
        }
    }
}