use chrono::DateTime;
use chrono_tz::Tz;
use crate::location::Location;
use crate::pvsystem::PVSystem;
use crate::solarposition::get_solarposition;
use crate::irradiance::{
aoi, get_total_irradiance, get_extra_radiation, poa_direct, erbs,
DiffuseModel, PoaComponents,
};
use crate::atmosphere::{get_relative_airmass, get_absolute_airmass, alt2pres};
use crate::iam;
use crate::temperature;
use crate::inverter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DCModel {
PVWatts,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ACModel {
PVWatts,
Sandia,
ADR,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AOIModel {
Physical,
ASHRAE,
SAPM,
MartinRuiz,
NoLoss,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SpectralModel {
NoLoss,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(non_camel_case_types)]
#[non_exhaustive]
pub enum TemperatureModel {
SAPM,
PVSyst,
Faiman,
Fuentes,
NOCT_SAM,
PVWatts,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TranspositionModel {
Isotropic,
HayDavies,
Perez,
Klucher,
Reindl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LossesModel {
PVWatts,
NoLoss,
}
#[derive(Debug, Clone)]
pub struct ModelChainConfig {
pub dc_model: DCModel,
pub ac_model: ACModel,
pub aoi_model: AOIModel,
pub spectral_model: SpectralModel,
pub temperature_model: TemperatureModel,
pub transposition_model: TranspositionModel,
pub losses_model: LossesModel,
}
#[derive(Debug, Clone)]
pub struct WeatherInput {
pub time: DateTime<Tz>,
pub ghi: Option<f64>,
pub dni: Option<f64>,
pub dhi: Option<f64>,
pub temp_air: f64,
pub wind_speed: f64,
pub albedo: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct POAInput {
pub time: DateTime<Tz>,
pub poa_direct: f64,
pub poa_diffuse: f64,
pub poa_global: f64,
pub temp_air: f64,
pub wind_speed: f64,
pub aoi: f64,
}
#[derive(Debug, Clone)]
pub struct EffectiveIrradianceInput {
pub time: DateTime<Tz>,
pub effective_irradiance: f64,
pub poa_global: f64,
pub temp_air: f64,
pub wind_speed: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModelChainResult {
pub solar_zenith: f64,
pub solar_azimuth: f64,
pub airmass: f64,
pub aoi: f64,
pub poa_global: f64,
pub poa_direct: f64,
pub poa_diffuse: f64,
pub aoi_modifier: f64,
pub spectral_modifier: f64,
pub effective_irradiance: f64,
pub cell_temperature: f64,
pub dc_power: f64,
pub ac_power: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SimulationResult {
pub poa_global: f64,
pub temp_cell: f64,
pub dc_power: f64,
pub ac_power: f64,
}
pub struct ModelChain {
pub system: PVSystem,
pub location: Location,
pub surface_tilt: f64,
pub surface_azimuth: f64,
pub inverter_pac0: f64,
pub inverter_eta: f64,
pub config: ModelChainConfig,
}
impl ModelChain {
pub fn new(
system: PVSystem,
location: Location,
surface_tilt: f64,
surface_azimuth: f64,
inverter_pac0: f64,
inverter_eta: f64,
) -> Self {
Self {
system,
location,
surface_tilt,
surface_azimuth,
inverter_pac0,
inverter_eta,
config: ModelChainConfig {
dc_model: DCModel::PVWatts,
ac_model: ACModel::PVWatts,
aoi_model: AOIModel::ASHRAE,
spectral_model: SpectralModel::NoLoss,
temperature_model: TemperatureModel::PVWatts,
transposition_model: TranspositionModel::Isotropic,
losses_model: LossesModel::NoLoss,
},
}
}
pub fn with_config(
system: PVSystem,
location: Location,
surface_tilt: f64,
surface_azimuth: f64,
inverter_pac0: f64,
inverter_eta: f64,
config: ModelChainConfig,
) -> Self {
Self {
system,
location,
surface_tilt,
surface_azimuth,
inverter_pac0,
inverter_eta,
config,
}
}
pub fn with_pvwatts(
system: PVSystem,
location: Location,
surface_tilt: f64,
surface_azimuth: f64,
inverter_pac0: f64,
inverter_eta: f64,
) -> Self {
Self {
system,
location,
surface_tilt,
surface_azimuth,
inverter_pac0,
inverter_eta,
config: ModelChainConfig {
dc_model: DCModel::PVWatts,
ac_model: ACModel::PVWatts,
aoi_model: AOIModel::Physical,
spectral_model: SpectralModel::NoLoss,
temperature_model: TemperatureModel::PVWatts,
transposition_model: TranspositionModel::Perez,
losses_model: LossesModel::PVWatts,
},
}
}
pub fn with_sapm(
system: PVSystem,
location: Location,
surface_tilt: f64,
surface_azimuth: f64,
inverter_pac0: f64,
inverter_eta: f64,
) -> Self {
Self {
system,
location,
surface_tilt,
surface_azimuth,
inverter_pac0,
inverter_eta,
config: ModelChainConfig {
dc_model: DCModel::PVWatts,
ac_model: ACModel::PVWatts,
aoi_model: AOIModel::ASHRAE,
spectral_model: SpectralModel::NoLoss,
temperature_model: TemperatureModel::SAPM,
transposition_model: TranspositionModel::HayDavies,
losses_model: LossesModel::NoLoss,
},
}
}
pub fn run_model(
&self,
time: DateTime<Tz>,
_ghi: f64,
dni: f64,
dhi: f64,
temp_air: f64,
_wind_speed: f64,
) -> Result<SimulationResult, spa::SpaError> {
let solpos = get_solarposition(&self.location, time)?;
let incidence = aoi(self.surface_tilt, self.surface_azimuth, solpos.zenith, solpos.azimuth);
let iam_mult = iam::ashrae(incidence, 0.05);
let poa_diffuse_val = crate::irradiance::isotropic(self.surface_tilt, dhi);
let poa_dir = poa_direct(incidence, dni);
let poa_global = poa_dir * iam_mult + poa_diffuse_val;
let temp_cell = temp_air + poa_global * (45.0 - 20.0) / 800.0;
let pdc = self.system.get_dc_power_total(poa_global, temp_cell);
let pdc0 = self.system.get_nameplate_dc_total();
let eta_inv_nom = self.inverter_eta;
let eta_inv_ref = 0.9637;
let pac = inverter::pvwatts_ac(pdc, pdc0, eta_inv_nom, eta_inv_ref);
Ok(SimulationResult {
poa_global,
temp_cell,
dc_power: pdc,
ac_power: pac,
})
}
pub fn run_model_from_weather(
&self,
weather: &WeatherInput,
) -> Result<ModelChainResult, spa::SpaError> {
let (ghi, dni, dhi) = self.resolve_irradiance(weather)?;
let albedo = weather.albedo.unwrap_or(0.25);
let solpos = get_solarposition(&self.location, weather.time)?;
let am_rel = get_relative_airmass(solpos.zenith);
let pressure = alt2pres(self.location.altitude);
let am_abs = if am_rel.is_nan() {
0.0
} else {
get_absolute_airmass(am_rel, pressure)
};
let aoi_val = aoi(self.surface_tilt, self.surface_azimuth, solpos.zenith, solpos.azimuth);
let day_of_year = {
use chrono::Datelike;
weather.time.ordinal() as i32
};
let dni_extra = get_extra_radiation(day_of_year);
let diffuse_model = match self.config.transposition_model {
TranspositionModel::Isotropic => DiffuseModel::Isotropic,
TranspositionModel::HayDavies => DiffuseModel::HayDavies,
TranspositionModel::Perez => DiffuseModel::Perez,
TranspositionModel::Klucher => DiffuseModel::Klucher,
TranspositionModel::Reindl => DiffuseModel::Reindl,
};
let poa = get_total_irradiance(
self.surface_tilt,
self.surface_azimuth,
solpos.zenith,
solpos.azimuth,
dni,
ghi,
dhi,
albedo,
diffuse_model,
Some(dni_extra),
if am_rel.is_nan() { None } else { Some(am_rel) },
);
self.compute_from_poa(
solpos.zenith,
solpos.azimuth,
am_abs,
aoi_val,
&poa,
weather.temp_air,
weather.wind_speed,
)
}
pub fn run_model_from_poa(
&self,
input: &POAInput,
) -> Result<ModelChainResult, spa::SpaError> {
let solpos = get_solarposition(&self.location, input.time)?;
let am_rel = get_relative_airmass(solpos.zenith);
let pressure = alt2pres(self.location.altitude);
let am_abs = if am_rel.is_nan() { 0.0 } else { get_absolute_airmass(am_rel, pressure) };
let poa = PoaComponents {
poa_global: input.poa_global,
poa_direct: input.poa_direct,
poa_diffuse: input.poa_diffuse,
poa_sky_diffuse: input.poa_diffuse,
poa_ground_diffuse: 0.0,
};
self.compute_from_poa(
solpos.zenith,
solpos.azimuth,
am_abs,
input.aoi,
&poa,
input.temp_air,
input.wind_speed,
)
}
pub fn run_model_from_effective_irradiance(
&self,
input: &EffectiveIrradianceInput,
) -> Result<ModelChainResult, spa::SpaError> {
let solpos = get_solarposition(&self.location, input.time)?;
let am_rel = get_relative_airmass(solpos.zenith);
let pressure = alt2pres(self.location.altitude);
let am_abs = if am_rel.is_nan() { 0.0 } else { get_absolute_airmass(am_rel, pressure) };
let temp_cell = self.calc_cell_temperature(
input.poa_global,
input.temp_air,
input.wind_speed,
);
let pdc = self.calc_dc_power(input.effective_irradiance, temp_cell);
let pac = self.calc_ac_power(pdc);
Ok(ModelChainResult {
solar_zenith: solpos.zenith,
solar_azimuth: solpos.azimuth,
airmass: am_abs,
aoi: 0.0,
poa_global: input.poa_global,
poa_direct: 0.0,
poa_diffuse: 0.0,
aoi_modifier: 1.0,
spectral_modifier: 1.0,
effective_irradiance: input.effective_irradiance,
cell_temperature: temp_cell,
dc_power: pdc,
ac_power: pac,
})
}
pub fn complete_irradiance(
&self,
weather: &WeatherInput,
) -> Result<(f64, f64, f64), spa::SpaError> {
self.resolve_irradiance(weather)
}
fn resolve_irradiance(
&self,
weather: &WeatherInput,
) -> Result<(f64, f64, f64), spa::SpaError> {
match (weather.ghi, weather.dni, weather.dhi) {
(Some(ghi), Some(dni), Some(dhi)) => Ok((ghi, dni, dhi)),
(Some(ghi), _, _) => {
let solpos = get_solarposition(&self.location, weather.time)?;
let day_of_year = {
use chrono::Datelike;
weather.time.ordinal() as i32
};
let dni_extra = get_extra_radiation(day_of_year);
let (dni, dhi) = erbs(ghi, solpos.zenith, day_of_year as u32, dni_extra);
Ok((ghi, dni, dhi))
}
(None, Some(dni), Some(dhi)) => {
let solpos = get_solarposition(&self.location, weather.time)?;
let cos_z = solpos.zenith.to_radians().cos().max(0.0);
let ghi = dni * cos_z + dhi;
Ok((ghi, dni, dhi))
}
_ => {
Ok((0.0, 0.0, 0.0))
}
}
}
#[allow(clippy::too_many_arguments)]
fn compute_from_poa(
&self,
solar_zenith: f64,
solar_azimuth: f64,
airmass: f64,
aoi_val: f64,
poa: &PoaComponents,
temp_air: f64,
wind_speed: f64,
) -> Result<ModelChainResult, spa::SpaError> {
let aoi_modifier = self.calc_aoi_modifier(aoi_val);
let spectral_modifier = self.calc_spectral_modifier();
let effective_irradiance = (poa.poa_direct * aoi_modifier + poa.poa_diffuse)
* spectral_modifier;
let temp_cell = self.calc_cell_temperature(poa.poa_global, temp_air, wind_speed);
let pdc = self.calc_dc_power(effective_irradiance, temp_cell);
let pac = self.calc_ac_power(pdc);
Ok(ModelChainResult {
solar_zenith,
solar_azimuth,
airmass,
aoi: aoi_val,
poa_global: poa.poa_global,
poa_direct: poa.poa_direct,
poa_diffuse: poa.poa_diffuse,
aoi_modifier,
spectral_modifier,
effective_irradiance,
cell_temperature: temp_cell,
dc_power: pdc,
ac_power: pac,
})
}
fn calc_aoi_modifier(&self, aoi_val: f64) -> f64 {
match self.config.aoi_model {
AOIModel::Physical => iam::physical(aoi_val, 1.526, 4.0, 0.002),
AOIModel::ASHRAE => iam::ashrae(aoi_val, 0.05),
AOIModel::MartinRuiz => iam::martin_ruiz(aoi_val, 0.16),
AOIModel::SAPM => {
iam::sapm(aoi_val, 1.0, -0.002438, 3.103e-4, -1.246e-5, 2.112e-7, -1.359e-9)
}
AOIModel::NoLoss => 1.0,
}
}
fn calc_spectral_modifier(&self) -> f64 {
match self.config.spectral_model {
SpectralModel::NoLoss => 1.0,
}
}
fn calc_cell_temperature(&self, poa_global: f64, temp_air: f64, wind_speed: f64) -> f64 {
match self.config.temperature_model {
TemperatureModel::SAPM => {
let (temp_cell, _) = temperature::sapm_cell_temperature(
poa_global, temp_air, wind_speed, -3.56, -0.075, 3.0, 1000.0,
);
temp_cell
}
TemperatureModel::PVSyst => {
temperature::pvsyst_cell_temperature(
poa_global, temp_air, wind_speed, 29.0, 0.0, 0.15, 0.9,
)
}
TemperatureModel::Faiman => {
temperature::faiman(poa_global, temp_air, wind_speed, 25.0, 6.84)
}
TemperatureModel::Fuentes => {
temperature::fuentes(poa_global, temp_air, wind_speed, 45.0)
}
TemperatureModel::NOCT_SAM => {
temperature::noct_sam_default(poa_global, temp_air, wind_speed, 45.0, 0.15)
}
TemperatureModel::PVWatts => {
temp_air + poa_global * (45.0 - 20.0) / 800.0
}
}
}
fn calc_dc_power(&self, effective_irradiance: f64, temp_cell: f64) -> f64 {
match self.config.dc_model {
DCModel::PVWatts => self.system.get_dc_power_total(effective_irradiance, temp_cell),
}
}
fn calc_ac_power(&self, pdc: f64) -> f64 {
let pdc0 = self.system.get_nameplate_dc_total();
let eta_inv_nom = self.inverter_eta;
let eta_inv_ref = 0.9637;
match self.config.ac_model {
ACModel::PVWatts => inverter::pvwatts_ac(pdc, pdc0, eta_inv_nom, eta_inv_ref),
ACModel::Sandia | ACModel::ADR => {
panic!(
"ACModel::{:?} is selected on ModelChain but is not wired up in \
this release — construct the inverter parameters and call \
`inverter::sandia` or `inverter::adr` directly, or use \
`ACModel::PVWatts`.",
self.config.ac_model
);
}
}
}
}