CoreCneRemedialActionsCreator.java

/*
 * Copyright (c) 2022, 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.openrao.data.raoresult.io.cne.core;

import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.TsoEICode;
import com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider;
import com.powsybl.openrao.data.raoresult.io.cne.commons.CneHelper;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.ConstraintSeries;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.ContingencySeries;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.RemedialActionRegisteredResource;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.RemedialActionSeries;
import com.powsybl.contingency.Contingency;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkAction;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;
import com.powsybl.openrao.data.crac.io.commons.api.ElementaryCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.PstRangeActionCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.UcteCracCreationContext;

import java.util.*;

import static com.powsybl.openrao.data.raoresult.io.cne.commons.CneConstants.*;
import static com.powsybl.openrao.data.raoresult.io.cne.commons.CneUtil.cutString;
import static com.powsybl.openrao.data.raoresult.io.cne.commons.CneUtil.randomizeString;
import static com.powsybl.openrao.data.raoresult.io.cne.core.CoreCneClassCreator.*;

/**
 * @author Viktor Terrier {@literal <viktor.terrier at rte-france.com>}
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 */
public final class CoreCneRemedialActionsCreator {

    private CneHelper cneHelper;
    private UcteCracCreationContext cracCreationContext;
    private List<ConstraintSeries> cnecsConstraintSeries;

    private static final String RA_SERIES = "RAseries";

    public CoreCneRemedialActionsCreator(CneHelper cneHelper, UcteCracCreationContext cracCreationContext, List<ConstraintSeries> cnecsConstraintSeries) {
        this.cneHelper = cneHelper;
        this.cnecsConstraintSeries = new ArrayList<>(cnecsConstraintSeries);
        this.cracCreationContext = cracCreationContext;
    }

    private CoreCneRemedialActionsCreator() {

    }

    /**
     * Creates RA ConstraintSeries for all RAs (B56) and adds them to the list
     * PS: This also adds the RemedialActionSeries to the CNECs' ConstraintSeries in the list,
     * so it should be done after adding the CNECs' ConstraintSeries to the list
     *
     * @return a List of ConstraintSeries
     */
    public List<ConstraintSeries> generate() {
        List<ConstraintSeries> constraintSeries = new ArrayList<>();

        List<PstRangeAction> sortedRangeActions = cracCreationContext.getRemedialActionCreationContexts().stream()
                .sorted(Comparator.comparing(ElementaryCreationContext::getNativeObjectId))
                .map(raCreationContext -> cneHelper.getCrac().getPstRangeAction(raCreationContext.getCreatedObjectId()))
                .filter(ra -> !Objects.isNull(ra))
                .toList();
        List<NetworkAction> sortedNetworkActions = cracCreationContext.getRemedialActionCreationContexts().stream()
                .sorted(Comparator.comparing(ElementaryCreationContext::getNativeObjectId))
                .map(raCreationContext -> cneHelper.getCrac().getNetworkAction(raCreationContext.getCreatedObjectId()))
                .filter(ra -> !Objects.isNull(ra))
                .toList();

        logMissingRangeActions();
        List<PstRangeAction> usedRangeActions = sortedRangeActions.stream().filter(this::isRangeActionUsedInRao).toList();
        if (!usedRangeActions.isEmpty()) {
            constraintSeries.add(createPreOptimRaConstraintSeries(usedRangeActions));
        }
        ConstraintSeries postPraB56 = createPostPraRaConstraintSeries(sortedRangeActions, sortedNetworkActions);
        if (!postPraB56.getRemedialActionSeries().isEmpty()) {
            constraintSeries.add(postPraB56);
        }
        constraintSeries.addAll(createPostCraRaConstraintSeries(sortedRangeActions, sortedNetworkActions));

        return constraintSeries;
    }

    private void logMissingRangeActions() {
        cracCreationContext.getRemedialActionCreationContexts().forEach(remedialActionCreationContext -> {
            if (!remedialActionCreationContext.isImported()) {
                OpenRaoLoggerProvider.TECHNICAL_LOGS.warn("Remedial action {} was not imported into the RAO, it will be absent from the CNE file", remedialActionCreationContext.getNativeObjectId());
            }
        });
    }

    private ConstraintSeries createPreOptimRaConstraintSeries(List<PstRangeAction> sortedRangeActions) {
        ConstraintSeries preOptimB56 = newConstraintSeries(randomizeString(RA_SERIES, 20), B56_BUSINESS_TYPE);
        sortedRangeActions.forEach(rangeAction -> preOptimB56.getRemedialActionSeries().add(createPreOptimRangeRemedialActionSeries(rangeAction)));
        return preOptimB56;
    }

    private boolean isRangeActionUsedInRao(PstRangeAction pstRangeAction) {
        return cneHelper.getCrac().getStates().stream().anyMatch(state -> cneHelper.getRaoResult().isActivatedDuringState(state, pstRangeAction));
    }

    private RemedialActionSeries createPreOptimRangeRemedialActionSeries(PstRangeAction pstRangeAction) {
        PstRangeActionCreationContext context = (PstRangeActionCreationContext) cracCreationContext.getRemedialActionCreationContexts().stream().filter(raContext -> pstRangeAction.getId().equals(raContext.getCreatedObjectId())).findFirst().orElseThrow();
        int initialTap = (context.isInverted() ? -1 : 1) * pstRangeAction.getInitialTap();
        RemedialActionSeries remedialActionSeries = createB56RemedialActionSeries(pstRangeAction.getId(), pstRangeAction.getName(), pstRangeAction.getOperator(), null);
        pstRangeAction.getNetworkElements().forEach(networkElement -> {
            RemedialActionRegisteredResource registeredResource = newRemedialActionRegisteredResource(context.getNativeObjectId(), context.getNativeNetworkElementId(),
                    PST_RANGE_PSR_TYPE, initialTap, WITHOUT_UNIT_SYMBOL, ABSOLUTE_MARKET_OBJECT_STATUS);
            remedialActionSeries.getRegisteredResource().add(registeredResource);
            remedialActionSeries.setMRID(createRangeActionId(remedialActionSeries.getMRID()));
        });
        return remedialActionSeries;
    }

    private ConstraintSeries createPostPraRaConstraintSeries(List<PstRangeAction> sortedRangeActions, List<NetworkAction> sortedNetworkActions) {
        ConstraintSeries preventiveB56 = newConstraintSeries(randomizeString(RA_SERIES, 20), B56_BUSINESS_TYPE);
        sortedRangeActions.forEach(rangeAction -> createPostOptimPstRangeActionSeries(rangeAction, InstantKind.PREVENTIVE, cneHelper.getCrac().getPreventiveState(), preventiveB56));
        sortedNetworkActions.forEach(networkAction -> createPostOptimNetworkRemedialActionSeries(networkAction, InstantKind.PREVENTIVE, cneHelper.getCrac().getPreventiveState(), preventiveB56));

        // Add the remedial action series to B54 and B57
        List<ConstraintSeries> basecaseConstraintSeriesList = cnecsConstraintSeries.stream()
                .filter(constraintSeries -> constraintSeries.getBusinessType().equals(B54_BUSINESS_TYPE) || constraintSeries.getBusinessType().equals(B57_BUSINESS_TYPE))
                .toList();
        addRemedialActionsToOtherConstraintSeries(preventiveB56.getRemedialActionSeries(), basecaseConstraintSeriesList);
        return preventiveB56;
    }

    private List<ConstraintSeries> createPostCraRaConstraintSeries(List<PstRangeAction> sortedRangeActions, List<NetworkAction> sortedNetworkActions) {
        List<ConstraintSeries> constraintSeriesList = new ArrayList<>();
        cneHelper.getCrac().getContingencies().stream().sorted(Comparator.comparing(Contingency::getId)).forEach(contingency -> {
            State curativeState = cneHelper.getCrac().getState(contingency.getId(), cneHelper.getCrac().getInstant(InstantKind.CURATIVE));
            if (curativeState == null) {
                return;
            }
            ConstraintSeries curativeB56 = newConstraintSeries(randomizeString(RA_SERIES, 20), B56_BUSINESS_TYPE);
            ContingencySeries contingencySeries = newContingencySeries(contingency.getId(), contingency.getName().orElse(contingency.getId()));
            curativeB56.getContingencySeries().add(contingencySeries);
            sortedRangeActions.forEach(rangeAction -> createPostOptimPstRangeActionSeries(rangeAction, InstantKind.CURATIVE, curativeState, curativeB56));
            sortedNetworkActions.forEach(networkAction -> createPostOptimNetworkRemedialActionSeries(networkAction, InstantKind.CURATIVE, curativeState, curativeB56));
            if (!curativeB56.getRemedialActionSeries().isEmpty()) {
                // Add remedial actions to corresponding CNECs' B54
                List<ConstraintSeries> contingencyConstraintSeriesList = cnecsConstraintSeries.stream()
                        .filter(constraintSeries -> constraintSeries.getBusinessType().equals(B54_BUSINESS_TYPE)
                                && constraintSeries.getContingencySeries().stream().anyMatch(series -> series.getName().equals(contingency.getName().orElse(contingency.getId()))))
                        .toList();
                addRemedialActionsToOtherConstraintSeries(curativeB56.getRemedialActionSeries(), contingencyConstraintSeriesList);
                // Add B56 to document
                constraintSeriesList.add(curativeB56);
            }
        });
        return constraintSeriesList;
    }

    public void createPostOptimPstRangeActionSeries(PstRangeAction rangeAction, InstantKind optimizedInstantKind, State state, ConstraintSeries constraintSeriesB56) {
        if (rangeAction.getUsageRules().stream().noneMatch(usageRule ->
                usageRule.getUsageMethod(state).equals(UsageMethod.AVAILABLE) || usageRule.getUsageMethod(state).equals(UsageMethod.FORCED))) {
            return;
        }
        // using RaoResult.isActivatedDuringState may throw an exception
        // if the state was not optimized or if the Range action was filtered out
        // that's why we use getActivatedRangeActionsDuringState instead
        boolean isActivated = cneHelper.getRaoResult().getActivatedRangeActionsDuringState(state).contains(rangeAction);
        if (isActivated && !rangeAction.getNetworkElements().isEmpty()) {
            RemedialActionSeries remedialActionSeries = createB56RemedialActionSeries(rangeAction.getId(), rangeAction.getName(), rangeAction.getOperator(), optimizedInstantKind);
            createPstRangeActionRegisteredResource(rangeAction, state, remedialActionSeries);
            constraintSeriesB56.getRemedialActionSeries().add(remedialActionSeries);
        }
    }

    private RemedialActionSeries createB56RemedialActionSeries(String remedialActionId, String remedialActionName, String operator, InstantKind optimizedInstantKind) {
        String marketObjectStatus = null;
        if (optimizedInstantKind != null) {
            switch (optimizedInstantKind) {
                case PREVENTIVE:
                    marketObjectStatus = PREVENTIVE_MARKET_OBJECT_STATUS;
                    break;
                case CURATIVE:
                    marketObjectStatus = CURATIVE_MARKET_OBJECT_STATUS;
                    break;
                default:
                    throw new OpenRaoException("Unknown CNE state");
            }
        }

        RemedialActionSeries remedialActionSeries = newRemedialActionSeries(remedialActionId, remedialActionName, marketObjectStatus);

        try {
            if (!Objects.isNull(operator)) {
                remedialActionSeries.getPartyMarketParticipant().add(newPartyMarketParticipant(TsoEICode.fromShortId(operator).getEICode()));
            }
        } catch (IllegalArgumentException e) {
            OpenRaoLoggerProvider.TECHNICAL_LOGS.warn(String.format("Operator %s is not a country id.", operator));
        }

        return remedialActionSeries;
    }

    private void fillB56ConstraintSeries(String remedialActionId, String remedialActionName, String operator, InstantKind optimizedInstantKind, ConstraintSeries constraintSeries) {
        RemedialActionSeries remedialActionSeries = createB56RemedialActionSeries(remedialActionId, remedialActionName, operator, optimizedInstantKind);
        constraintSeries.getRemedialActionSeries().add(remedialActionSeries);
    }

    private void createPstRangeActionRegisteredResource(PstRangeAction pstRangeAction, State state, RemedialActionSeries remedialActionSeries) {
        PstRangeActionCreationContext context = (PstRangeActionCreationContext) cracCreationContext.getRemedialActionCreationContexts().stream().filter(raContext -> pstRangeAction.getId().equals(raContext.getCreatedObjectId())).findFirst().orElseThrow();
        int tap = (context.isInverted() ? -1 : 1) * cneHelper.getRaoResult().getOptimizedTapOnState(state, pstRangeAction);
        RemedialActionRegisteredResource registeredResource = newRemedialActionRegisteredResource(context.getNativeObjectId(), context.getNativeNetworkElementId(), PST_RANGE_PSR_TYPE, tap, WITHOUT_UNIT_SYMBOL, ABSOLUTE_MARKET_OBJECT_STATUS);
        remedialActionSeries.getRegisteredResource().add(registeredResource);
        remedialActionSeries.setMRID(createRangeActionId(remedialActionSeries.getMRID()));
    }

    private String createRangeActionId(String mRid) {
        return cutString(mRid, 55);
    }

    public void createPostOptimNetworkRemedialActionSeries(NetworkAction networkAction, InstantKind optimizedInstantKind, State state, ConstraintSeries constraintSeriesB56) {
        if (networkAction.getUsageRules().stream().noneMatch(usageRule ->
                usageRule.getUsageMethod(state).equals(UsageMethod.AVAILABLE) || usageRule.getUsageMethod(state).equals(UsageMethod.FORCED))) {
            return;
        }
        // using RaoResult.isActivatedDuringState may throw an exception
        // if the state was not optimized
        // that's why we use getActivatedNetworkActionsDuringState instead
        boolean isActivated = cneHelper.getRaoResult().getActivatedNetworkActionsDuringState(state).contains(networkAction);
        if (isActivated && !networkAction.getNetworkElements().isEmpty()) {
            fillB56ConstraintSeries(networkAction.getId(), networkAction.getName(), networkAction.getOperator(), optimizedInstantKind, constraintSeriesB56);
        }
    }

    public void addRemedialActionsToOtherConstraintSeries(List<RemedialActionSeries> remedialActionSeriesList, List<ConstraintSeries> constraintSeriesList) {
        remedialActionSeriesList.forEach(remedialActionSeries -> {
            RemedialActionSeries shortPostOptimRemedialActionSeries = newRemedialActionSeries(remedialActionSeries.getMRID(), remedialActionSeries.getName(), remedialActionSeries.getApplicationModeMarketObjectStatusStatus());
            constraintSeriesList.stream().forEach(constraintSeries -> constraintSeries.getRemedialActionSeries().add(shortPostOptimRemedialActionSeries));
        });
    }
}