Source code for exerpy.components.solar_thermal.heliostatfield
import numpy as np
from exerpy.components.component import Component
from exerpy.components.component import component_registry
from exerpy.functions import fluid_property_data
from exerpy.logger import logger
from . import T_SUN
[docs]
@component_registry
class Heliostatfield(Component):
r"""
Class for exergy analysis of a heliostat field.
The heliostat field concentrates incoming solar radiation and delivers heat to the
central receiver. The fuel is the exergy of the incoming solar heat and the product is
the exergy of the heat effectively delivered to the receiver; their difference is the
optical loss of the field. Radiation is converted to exergy with the Petela/Spanner factor
:math:`\alpha = 1 - \frac{4}{3}\,T_0 / T_\mathrm{sun}`.
All thermal and optical losses are accounted as exergy destruction
(:math:`\dot{E}_\mathrm{D}`), not as a separate exergy loss.
Attributes
----------
E_F : float
Exergy fuel of the component :math:`\dot{E}_\mathrm{F}` in :math:`\mathrm{W}`.
E_P : float
Exergy product of the component :math:`\dot{E}_\mathrm{P}` in :math:`\mathrm{W}`.
E_D : float
Exergy destruction of the component :math:`\dot{E}_\mathrm{D}` in :math:`\mathrm{W}`.
epsilon : float
Exergetic efficiency of the component :math:`\varepsilon` in :math:`-`.
Q_Solar : float
Incoming solar heat input :math:`\dot{Q}_\mathrm{solar}` in :math:`\mathrm{W}`.
P : float
Effective heat delivered to the receiver :math:`\dot{Q}_\mathrm{eff}` in :math:`\mathrm{W}`.
inl : dict
Inlet connections (a heliostat field has no material inlet).
outl : dict
Heat outlet connections: the heat delivered to the receiver and the incoming solar
heat (both carry heat, no mass flow).
"""
def __init__(self, **kwargs):
r"""
Initialize the heliostat field component.
Parameters
----------
Q_Solar : float, optional
Incoming solar heat input in :math:`\mathrm{W}`.
**kwargs : dict
Arbitrary keyword arguments passed to the parent class.
"""
super().__init__(**kwargs)
self.Q_Solar = kwargs.get("Q_Solar")
self.P = None
[docs]
def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None:
r"""
Calculate the exergy balance of the heliostat field.
Parameters
----------
T0 : float
Ambient temperature in :math:`\mathrm{K}`.
p0 : float
Ambient pressure in :math:`\mathrm{Pa}`.
split_physical_exergy : bool
Flag indicating whether physical exergy is split into thermal and mechanical parts.
Returns
-------
None
The method updates the attributes E_F, E_P, E_D, and epsilon of the component instance.
Raises
------
ValueError
If the component has no outlet connections attribute, or no valid heat output to
the receiver.
Notes
-----
Solar radiation is converted to exergy with the Petela/Spanner factor
:math:`\alpha = 1 - \frac{4}{3}\,T_0 / T_\mathrm{sun}`, with
:math:`T_\mathrm{sun} = 5778\ \mathrm{K}`.
- Exergy fuel: the incoming solar heat exergy,
:math:`\dot{E}_\mathrm{F} = \dot{Q}_\mathrm{solar}\,\alpha`.
- Exergy product: the exergy of the heat delivered to the receiver,
:math:`\dot{E}_\mathrm{P} = \dot{Q}_\mathrm{eff}\,\alpha`.
- Exergy destruction: :math:`\dot{E}_\mathrm{D} = \dot{E}_\mathrm{F} - \dot{E}_\mathrm{P}`.
- Exergetic efficiency: :math:`\varepsilon = \dot{E}_\mathrm{P} / \dot{E}_\mathrm{F}`.
If the field has no outlet connections, all exergy values are set to zero and epsilon
to NaN.
"""
# Validate the number of inlets and outlets
if not hasattr(self, "outl"):
msg = f"Heliostat field {self.name} requires outlet connections."
logger.error(msg)
raise ValueError(msg)
# The heliostat field has heat outlet connection(s) only (the link to the receiver
# and the synthetic solar heat-flux connection) and no material inlet.
if len(self.outl) == 0:
logger.warning(f"Heliostat Field '{self.name}' has no outlet connections.")
self.E_F = 0
self.E_P = 0
self.E_D = 0
self.epsilon = np.nan
return
# Extract inlet and outlet streams
# Normalize placeholder zeros to None for safety (some parsers use 0 as placeholder)
if hasattr(self, "inl") and 0 in self.inl and self.inl[0] == 0:
self.inl[0] = None
if hasattr(self, "outl") and 0 in self.outl and self.outl[0] == 0:
self.outl[0] = None
# Effective heat delivered to the receiver is the internal heat outlet (outl[0]); use
# its raw heat flow (the Petela/Spanner factor is applied below).
if 0 in self.outl and self.outl[0] is not None and self.outl[0].get("kind") == "heat":
out0 = self.outl[0]
self.P = out0.get("energy_flow") if out0.get("energy_flow") is not None else out0.get("E")
if self.P is None:
msg = f"Heliostat field {self.name} has no valid heat output on its receiver link."
logger.error(msg)
raise ValueError(msg)
if self.Q_Solar is None:
msg = f"Heliostat field {self.name} has no solar heat input (Q_Solar)."
logger.error(msg)
raise ValueError(msg)
# Convert the incoming solar heat to exergy with the Petela/Spanner factor.
alpha = 1 - (4 / 3) * (T0 / T_SUN)
# Calculate exergy of incoming Heat
self.E_F = self.Q_Solar * alpha # Total incoming solar exergy
# Calculate exergy of effective Heat
# Use 'energy_flow' key for heat connections from the parser
self.E_P = self.P * alpha # Exergy of effective heat output
# Calculate exergy destruction
self.E_D = self.E_F - self.E_P
# Write the computed exergy flows back onto the heat connections so that the rest
# of the pipeline and the system-level E_F/E_P accounting can read them. Heat
# connections are created with E=None, so without this the solar heat carries no
# exergy and the system fuel would be zero.
#
# The incoming solar heat is modelled as a boundary heat connection (one side has
# no component), while the heat delivered to the receiver is an internal connection
# (both sides connected). Classify by topology rather than by inlet/outlet slot,
# because the synthetic solar connection is stored as an outlet here.
# - boundary solar heat connection -> fuel exergy E_F
# - internal heat connection (to receiver) -> product exergy E_P
try:
for conn in list(self.outl.values()) + list(self.inl.values()):
if conn is None or conn.get("kind") != "heat":
continue
if conn.get("source_component") is None or conn.get("target_component") is None:
conn["E"] = self.E_F
else:
conn["E"] = self.E_P
conn["E_unit"] = fluid_property_data["heat"]["SI_unit"]
except (KeyError, TypeError) as e:
logger.warning(f"Could not write back exergy to connection for Heliostat '{self.name}': {e}")
# Calculate exergy efficiency
self.epsilon = self.calc_epsilon()
# Log the results
logger.info(
f"Heliostat Field Exergy Balance: "
f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
f"Efficiency={self.epsilon:.2%}"
)
[docs]
def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
r"""Exergoeconomic auxiliary equations are not yet implemented for this component."""
raise NotImplementedError("Exergoeconomic analysis is not yet implemented for the Heliostatfield component.")
[docs]
def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False):
r"""Exergoeconomic balance is not yet implemented for this component."""
raise NotImplementedError("Exergoeconomic analysis is not yet implemented for the Heliostatfield component.")