use core::{fmt::Display, time::Duration};
use argmin::{
core::{CostFunction, Error, Executor},
solver::particleswarm::ParticleSwarm,
};
use enum_map::Enum;
use log::{debug, info, warn};
use micromath::F32Ext;
use nalgebra::{ArrayStorage, Const, Matrix};
use thiserror::Error;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
Atmosim,
config::R,
gases::{GAS_COUNT, Gas, GasFlags, GasMixture, Moles, gases},
maxcap::{GasContainer, GasContainerPrototype, GasContainerState},
};
const EXP_LIMIT: f32 = 6.;
const DUPLICATE_GAS_PENALTY: f32 = 0.1;
const EACH_GAS_PENALTY: f32 = 0.01;
#[derive(Debug, Error)]
#[error("Optimizer gave no plausible results")]
struct OptimizationError;
#[derive(Debug, Clone, Copy)]
pub enum OptimizeFor {
BiggerExplosion,
SlowerFuse,
FasterFuse,
}
#[derive(Debug, Clone, Copy)]
pub struct Optimizer<'a> {
pub engine: &'a Atmosim,
pub allowed_gases0: GasFlags,
pub temp_range0: (f32, f32),
pub allowed_gases1: GasFlags,
pub temp_range1: (f32, f32),
pub optimize_for: OptimizeFor,
pub tick_range: (usize, usize),
pub min_explosion_radius: f32,
pub minimal_part: f32,
pub pso_particles: usize,
pub timeout: Duration,
}
impl<'a> Optimizer<'a> {
#[must_use]
pub fn from_engine(engine: &'a Atmosim) -> Self {
Self {
engine,
allowed_gases0: gases! {
Gas::Ammonia | Gas::CarbonDioxide | Gas::Frezon |
Gas::Nitrogen | Gas::NitrousOxide | Gas::Plasma |
Gas::Tritium | Gas::WaterVapor => true,
_ => false,
},
temp_range0: (71.2, 595.1),
allowed_gases1: gases! {
Gas::Oxygen => true,
_ => false,
},
temp_range1: (293.15, 293.15),
optimize_for: OptimizeFor::BiggerExplosion,
tick_range: (0, 7200), min_explosion_radius: 4.,
minimal_part: 0.05,
pso_particles: 7777, timeout: Duration::from_secs(3),
}
}
}
type GasMatrix =
Matrix<f32, Const<{ GAS_COUNT + 1 }>, Const<2>, ArrayStorage<f32, { GAS_COUNT + 1 }, 2>>;
impl CostFunction for Optimizer<'_> {
type Param = GasMatrix;
type Output = f32;
fn cost(&self, param: &Self::Param) -> Result<Self::Output, Error> {
let (mut mix0, mut mix1) = self.matrix_to_mixes(param);
if mix0.react() || mix1.react() {
return Ok(f32::MAX); }
let total0 = mix0.total_moles();
let total1 = mix1.total_moles();
let control = (total0 + total1) * self.minimal_part;
if total0 < control || total1 < control {
return Ok(f32::MAX); }
let penalty = {
let each_penalty = mix0
.moles()
.values()
.zip(mix1.moles().values())
.filter(|&(&first, &second)| first > 0. || second > 0.)
.count() as f32
* EACH_GAS_PENALTY;
let duplicate_penalty = mix0
.moles()
.values()
.zip(mix1.moles().values())
.filter(|&(&first, &second)| first > 0. && second > 0.)
.count() as f32
* DUPLICATE_GAS_PENALTY;
each_penalty + duplicate_penalty
};
let (radius, ticks) = calculate_maxcap(mix0, mix1, self.tick_range.1);
Ok(if radius > 0. {
(match (self.tick_range.0, self.min_explosion_radius) {
(min_ticks, _) if ticks < min_ticks => f32::MAX, (_, min_radius) if radius < min_radius => f32::MAX, _ => match self.optimize_for {
OptimizeFor::BiggerExplosion => -radius,
OptimizeFor::SlowerFuse => -(ticks as f32),
OptimizeFor::FasterFuse => ticks as f32,
},
} + penalty) } else {
f32::MAX
})
}
}
fn calculate_maxcap(mix0: GasMixture, mix1: GasMixture, tick_limit: usize) -> (f32, usize) {
let mut container = GasContainer::new(mix0 + mix1);
container.step_n(tick_limit);
(
match container.state() {
GasContainerState::Exploded { radius } => radius,
_ => 0.,
},
container.ticks(),
)
}
fn bound(gases: GasFlags, i: usize, positive: bool) -> f32 {
if gases[Gas::from_usize(i)] {
if positive { EXP_LIMIT } else { -EXP_LIMIT }
} else {
f32::MIN
}
}
impl Optimizer<'_> {
pub fn run(&self) -> Result<MaxcapData, Error> {
let tr0 = self.temp_range0;
let tr1 = self.temp_range1;
if tr0.0 > tr0.1 || tr1.0 > tr1.1 {
Err(OptimizationError)?; }
let lower_bounds = {
GasMatrix::from_fn(|i, j| match (i, j) {
(..GAS_COUNT, 0) => bound(self.allowed_gases0, i, false),
(..GAS_COUNT, 1) => bound(self.allowed_gases1, i, false),
(GAS_COUNT, 0) => self.temp_range0.0,
(GAS_COUNT, 1) => self.temp_range1.0,
_ => unreachable!(),
})
};
let upper_bounds = {
GasMatrix::from_fn(|i, j| match (i, j) {
(..GAS_COUNT, 0) => bound(self.allowed_gases0, i, true),
(..GAS_COUNT, 1) => bound(self.allowed_gases1, i, true),
(GAS_COUNT, 0) => self.temp_range0.1,
(GAS_COUNT, 1) => self.temp_range1.1,
_ => unreachable!(),
})
};
let solver = ParticleSwarm::new((lower_bounds, upper_bounds), self.pso_particles);
let res = Executor::new(*self, solver).timeout(self.timeout).run()?;
debug!("{res}");
let cur = res.state.iter * self.pso_particles as u64;
let best = res.state.last_best_iter * self.pso_particles as u64;
if best * 3 / 2 > cur {
warn!(
"Processed {cur} maxcaps, but the best result was only found recently at {best}. Consider trying again with more time."
);
}
let (mix0, mix1) = match res.state.best_individual {
Some(particle) if particle.cost.is_sign_negative() => {
self.matrix_to_mixes(&particle.position)
}
_ => Err(OptimizationError)?, };
let (radius, ticks) = calculate_maxcap(mix0, mix1, self.tick_range.1);
info!("The result mixes down to {}K.", (mix0 + mix1).temperature());
Ok(MaxcapData {
moles0: mix0.moles(),
temp0: mix0.temperature(),
moles1: mix1.moles(),
temp1: mix1.temperature(),
radius,
ticks,
container_proto: self.engine.container(),
})
}
fn matrix_to_mixes<'a>(&'a self, matrix: &GasMatrix) -> (GasMixture<'a>, GasMixture<'a>) {
let mut mix0 = self.row_to_mix(&matrix.data.0[0]);
let mut mix1 = self.row_to_mix(&matrix.data.0[1]);
let ratio = {
let container = GasContainer::new(mix0 + mix1);
self.engine.container().normal_pressure / container.pressure()
};
mix0.modify_moles_each(|mole| mole * ratio);
mix1.modify_moles_each(|mole| mole * ratio);
(mix0, mix1)
}
fn row_to_mix<'a>(&'a self, row: &[f32]) -> GasMixture<'a> {
assert_eq!(row.len(), GAS_COUNT + 1); let (&temp, slice) = row.split_last().unwrap();
let mut moles = Moles::from_fn(|i| F32Ext::exp(slice[i.into_usize()]));
let total = moles.values().sum::<f32>();
for mole in moles.values_mut() {
if *mole < total * self.minimal_part {
*mole = 0.;
}
}
self.engine.create_mixture(moles, temp)
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MaxcapData {
pub moles0: Moles,
pub temp0: f32,
pub moles1: Moles,
pub temp1: f32,
pub radius: f32,
pub ticks: usize,
pub container_proto: GasContainerPrototype,
}
impl Display for MaxcapData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let moles0_sum = self.moles0.values().sum::<f32>();
let moles1_sum = self.moles1.values().sum::<f32>();
let pressure0 = moles0_sum * R * self.temp0 / self.container_proto.volume;
writeln!(
f,
"- MIX 1: {}K, fill up to {} kPa -",
self.temp0, pressure0
)?;
for (gas, amount) in self.moles0 {
if amount > f32::MIN_POSITIVE {
writeln!(
f,
"{gas:?}: {}% ({amount} moles)",
amount / moles0_sum * 100.
)?;
}
}
writeln!(
f,
"- MIX 2: {}K, fill up to {} kPa -",
self.temp1, self.container_proto.normal_pressure
)?;
for (gas, amount) in self.moles1 {
if amount > f32::MIN_POSITIVE {
writeln!(
f,
"{gas:?}: {}% ({amount} moles)",
amount / moles1_sum * 100.
)?;
}
}
write!(
f,
"- Explodes after {} ticks with {} tile radius -",
self.ticks, self.radius
)
}
}