LfLoadImpl.java
/**
* Copyright (c) 2021, 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/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.openloadflow.network.impl;
import com.powsybl.iidm.network.DanglingLine;
import com.powsybl.iidm.network.LccConverterStation;
import com.powsybl.iidm.network.Load;
import com.powsybl.iidm.network.LoadType;
import com.powsybl.iidm.network.extensions.LoadDetail;
import com.powsybl.iidm.network.util.HvdcUtils;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.util.Evaluable;
import com.powsybl.openloadflow.util.EvaluableConstants;
import com.powsybl.openloadflow.util.PerUnit;
import java.util.*;
import java.util.stream.Stream;
/**
* @author Anne Tilloy {@literal <anne.tilloy at rte-france.com>}
*/
public class LfLoadImpl extends AbstractLfInjection implements LfLoad {
private final LfBus bus;
private final LfLoadModel loadModel;
private final Map<String, Ref<Load>> loadsRefs = new HashMap<>();
private final List<Ref<LccConverterStation>> lccCsRefs = new ArrayList<>();
private double targetQ = 0;
private boolean ensurePowerFactorConstantByLoad = false;
private final HashMap<String, Double> loadsAbsVariableTargetP = new HashMap<>();
private double absVariableTargetP = 0;
private final boolean distributedOnConformLoad;
private Map<String, Boolean> loadsDisablingStatus = new LinkedHashMap<>();
private Evaluable p = EvaluableConstants.NAN;
private Evaluable q = EvaluableConstants.NAN;
LfLoadImpl(LfBus bus, boolean distributedOnConformLoad, LfLoadModel loadModel) {
super(0, 0);
this.bus = Objects.requireNonNull(bus);
this.distributedOnConformLoad = distributedOnConformLoad;
this.loadModel = loadModel;
}
@Override
public String getId() {
return bus.getId() + "_load";
}
@Override
public List<String> getOriginalIds() {
return Stream.concat(loadsRefs.values().stream().map(r -> r.get().getId()),
lccCsRefs.stream().map(r -> r.get().getId()))
.toList();
}
@Override
public LfBus getBus() {
return bus;
}
@Override
public boolean isOriginalLoadNotParticipating(String originalLoadId) {
if (loadsRefs.get(originalLoadId) == null) {
return false;
}
return isLoadNotParticipating(loadsRefs.get(originalLoadId).get());
}
@Override
public Optional<LfLoadModel> getLoadModel() {
return Optional.ofNullable(loadModel);
}
void add(Load load, LfNetworkParameters parameters) {
loadsRefs.put(load.getId(), Ref.create(load, parameters.isCacheEnabled()));
loadsDisablingStatus.put(load.getId(), false);
double p0 = load.getP0();
double q0 = load.getQ0();
targetP += p0 / PerUnit.SB;
initialTargetP += p0 / PerUnit.SB;
targetQ += q0 / PerUnit.SB;
boolean hasVariableActivePower = false;
if (parameters.isDistributedOnConformLoad()) {
LoadDetail loadDetail = load.getExtension(LoadDetail.class);
if (loadDetail != null) {
hasVariableActivePower = loadDetail.getFixedActivePower() != load.getP0();
}
}
boolean reactiveOnlyLoad = p0 == 0 && q0 != 0;
if (p0 < 0 || hasVariableActivePower || reactiveOnlyLoad) {
ensurePowerFactorConstantByLoad = true;
}
double absTargetP = getAbsVariableTargetPPerUnit(load, distributedOnConformLoad);
loadsAbsVariableTargetP.put(load.getId(), absTargetP);
absVariableTargetP += absTargetP;
}
void add(LccConverterStation lccCs, LfNetworkParameters parameters) {
// note that LCC converter station are out of the slack distribution.
lccCsRefs.add(Ref.create(lccCs, parameters.isCacheEnabled()));
double lccTargetP = HvdcUtils.getConverterStationTargetP(lccCs);
this.targetP += lccTargetP / PerUnit.SB;
initialTargetP += lccTargetP / PerUnit.SB;
targetQ += HvdcUtils.getLccConverterStationLoadTargetQ(lccCs) / PerUnit.SB;
}
public void add(DanglingLine danglingLine) {
targetP += danglingLine.getP0() / PerUnit.SB;
targetQ += danglingLine.getQ0() / PerUnit.SB;
}
@Override
public void setTargetP(double targetP) {
if (targetP != this.targetP) {
double oldTargetP = this.targetP;
this.targetP = targetP;
bus.invalidateLoadTargetP();
for (LfNetworkListener listener : bus.getNetwork().getListeners()) {
listener.onLoadActivePowerTargetChange(this, oldTargetP, targetP);
}
}
}
@Override
public double getTargetQ() {
return targetQ;
}
@Override
public void setTargetQ(double targetQ) {
if (targetQ != this.targetQ) {
double oldTargetQ = this.targetQ;
this.targetQ = targetQ;
bus.invalidateLoadTargetQ();
for (LfNetworkListener listener : bus.getNetwork().getListeners()) {
listener.onLoadReactivePowerTargetChange(this, oldTargetQ, targetQ);
}
}
}
@Override
public boolean ensurePowerFactorConstantByLoad() {
return ensurePowerFactorConstantByLoad;
}
@Override
public double getAbsVariableTargetP() {
return absVariableTargetP;
}
@Override
public void setAbsVariableTargetP(double absVariableTargetP) {
this.absVariableTargetP = absVariableTargetP;
}
public static double getAbsVariableTargetPPerUnit(Load load, boolean distributedOnConformLoad) {
if (isLoadNotParticipating(load)) {
return 0.0;
}
double varP;
if (distributedOnConformLoad) {
varP = load.getExtension(LoadDetail.class) == null ? 0 : load.getExtension(LoadDetail.class).getVariableActivePower();
} else {
varP = load.getP0();
}
return Math.abs(varP) / PerUnit.SB;
}
@Override
public int getOriginalLoadCount() {
return loadsRefs.size();
}
private double getParticipationFactor(String originalLoadId) {
// FIXME
// After a load contingency or a load action, only the global variable targetP is updated.
// The list loadsAbsVariableTargetP never changes. It is not an issue for security analysis as the network is
// never updated. Excepted if loadPowerFactorConstant is true, the new targetQ could be wrong after a load contingency
// or a load action.
return absVariableTargetP != 0 ? loadsAbsVariableTargetP.get(originalLoadId) / absVariableTargetP : 0;
}
private double calculateP() {
return p.eval() + getLoadModel()
.flatMap(lm -> lm.getExpTermP(0).map(term -> targetP * term.c()))
.orElse(0d);
}
private double calculateQ() {
return q.eval() + getLoadModel()
.flatMap(lm -> lm.getExpTermQ(0).map(term -> targetQ * term.c()))
.orElse(0d);
}
@Override
public void updateState(boolean loadPowerFactorConstant, boolean breakers) {
double pv = p == EvaluableConstants.NAN ? 1 : calculateP() / targetP; // extract part of p that is dependent to voltage
double qv = q == EvaluableConstants.NAN ? 1 : calculateQ() / targetQ;
double diffLoadTargetP = targetP - initialTargetP;
for (Ref<Load> refLoad : loadsRefs.values()) {
Load load = refLoad.get();
double diffP0 = diffLoadTargetP * getParticipationFactor(load.getId()) * PerUnit.SB;
double updatedP0 = load.getP0() + diffP0;
double updatedQ0 = load.getQ0() + (loadPowerFactorConstant ? getPowerFactor(load) * diffP0 : 0.0);
load.getTerminal()
.setP(updatedP0 * pv)
.setQ(updatedQ0 * qv);
}
// update lcc converter station power
for (Ref<LccConverterStation> lccCsRef : lccCsRefs) {
LccConverterStation lccCs = lccCsRef.get();
double pCs = HvdcUtils.getConverterStationTargetP(lccCs); // A LCC station has active losses.
double qCs = HvdcUtils.getLccConverterStationLoadTargetQ(lccCs); // A LCC station always consumes reactive power.
lccCs.getTerminal()
.setP(pCs)
.setQ(qCs);
}
}
@Override
public double calculateNewTargetQ(double diffTargetP) {
double newLoadTargetQ = 0;
for (Ref<Load> refLoad : loadsRefs.values()) {
Load load = refLoad.get();
double updatedQ0 = load.getQ0() / PerUnit.SB + getPowerFactor(load) * diffTargetP * getParticipationFactor(load.getId());
newLoadTargetQ += updatedQ0;
}
return newLoadTargetQ;
}
@Override
public boolean isOriginalLoadDisabled(String originalId) {
return loadsDisablingStatus.get(originalId);
}
@Override
public void setOriginalLoadDisabled(String originalId, boolean disabled) {
loadsDisablingStatus.put(originalId, disabled);
}
@Override
public Map<String, Boolean> getOriginalLoadsDisablingStatus() {
return loadsDisablingStatus;
}
@Override
public void setOriginalLoadsDisablingStatus(Map<String, Boolean> originalLoadsDisablingStatus) {
this.loadsDisablingStatus = Objects.requireNonNull(originalLoadsDisablingStatus);
}
private static double getPowerFactor(Load load) {
return load.getP0() != 0 ? load.getQ0() / load.getP0() : 1;
}
/**
* Returns true if the load does not participate to slack distribution
*/
public static boolean isLoadNotParticipating(Load load) {
// Fictitious loads that do not participate to slack distribution.
return isLoadFictitious(load);
}
/**
* Returns whether the load is tagged fictitious in the grid model
*/
public static boolean isLoadFictitious(Load load) {
// Fictitious loads that do not participate to slack distribution.
return load.isFictitious() || LoadType.FICTITIOUS.equals(load.getLoadType());
}
@Override
public double getNonFictitiousLoadTargetP() {
return loadsRefs.values().stream()
.map(Ref::get)
.filter(Objects::nonNull)
.filter(l -> !isLoadFictitious(l))
.mapToDouble(Load::getP0)
.sum();
}
@Override
public Evaluable getP() {
return p;
}
@Override
public void setP(Evaluable p) {
this.p = p;
}
@Override
public Evaluable getQ() {
return q;
}
@Override
public void setQ(Evaluable q) {
this.q = q;
}
@Override
public String toString() {
return getId();
}
}