starfall/astronomy/close_binary_star/constraints/
mod.rs

1use rand::prelude::*;
2use std::default::Default;
3
4use crate::astronomy::close_binary_star::constants::*;
5use crate::astronomy::close_binary_star::error::Error;
6use crate::astronomy::close_binary_star::CloseBinaryStar;
7use crate::astronomy::star::constraints::Constraints as StarConstraints;
8
9/// Constraints for creating a binary star.
10///
11/// As it turns out, randomly generating a habitable binary star is HARD.
12///
13/// The dwarf stars that are best for habitability cause an inward pressure
14/// on the habitable zone, but the gravitational field of the stars is very
15/// dangerous and chips away at the habitable zone from inside.
16///
17/// After beating my head against this for a while, I think the best approach
18/// is to contort the parameters to give a high rate of success, while sadly
19/// acknowledging that a lot of the potential variety has been squashed :(
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct Constraints {
22  /// The minimum combined mass of the stars, in Msol.
23  pub minimum_combined_mass: Option<f64>,
24  /// The maximum combined mass of the stars, in Msol.
25  pub maximum_combined_mass: Option<f64>,
26  /// The minimum individual mass of the stars, in Msol.
27  pub minimum_individual_mass: Option<f64>,
28  /// The maximum individual mass of the stars, in Msol.
29  pub maximum_individual_mass: Option<f64>,
30  /// The minimum separation between the stars, in Msol.
31  pub minimum_average_separation: Option<f64>,
32  /// The maximum separation between the stars, in Msol.
33  pub maximum_average_separation: Option<f64>,
34  /// The minimum orbital eccentricity.
35  pub minimum_orbital_eccentricity: Option<f64>,
36  /// The maximum orbital_eccentricity.
37  pub maximum_orbital_eccentricity: Option<f64>,
38  /// The minimum age of the stars, in Gyr.
39  pub minimum_age: Option<f64>,
40  /// The maximum age of the stars, in Gyr.
41  pub maximum_age: Option<f64>,
42  /// Enforce habitability.
43  pub enforce_habitability: bool,
44  /// Star constraints.
45  pub star_constraints: Option<StarConstraints>,
46}
47
48impl Constraints {
49  /// Generate a habitable binary star.
50  #[named]
51  pub fn habitable() -> Self {
52    trace_enter!();
53    let minimum_combined_mass = Some(MINIMUM_HABITABLE_COMBINED_MASS);
54    let maximum_combined_mass = Some(MAXIMUM_HABITABLE_COMBINED_MASS);
55    let minimum_individual_mass = Some(MINIMUM_HABITABLE_INDIVIDUAL_MASS);
56    let maximum_individual_mass = Some(MAXIMUM_HABITABLE_INDIVIDUAL_MASS);
57    let minimum_orbital_eccentricity = Some(MINIMUM_HABITABLE_ORBITAL_ECCENTRICITY);
58    let maximum_orbital_eccentricity = Some(MAXIMUM_HABITABLE_ORBITAL_ECCENTRICITY);
59    let maximum_average_separation = Some(MAXIMUM_HABITABLE_AVERAGE_SEPARATION);
60    let minimum_age = Some(MINIMUM_HABITABLE_AGE);
61    let enforce_habitability = true;
62    let star_constraints = Some(StarConstraints::habitable());
63    let result = Self {
64      minimum_combined_mass,
65      maximum_combined_mass,
66      minimum_individual_mass,
67      maximum_individual_mass,
68      maximum_average_separation,
69      minimum_orbital_eccentricity,
70      maximum_orbital_eccentricity,
71      minimum_age,
72      enforce_habitability,
73      star_constraints,
74      ..Constraints::default()
75    };
76    trace_var!(result);
77    trace_exit!();
78    result
79  }
80
81  /// Generate a binary star from our constraints.
82  #[named]
83  pub fn generate<R: Rng + ?Sized>(&self, rng: &mut R) -> Result<CloseBinaryStar, Error> {
84    trace_enter!();
85    let mut minimum_combined_mass = self.minimum_combined_mass.unwrap_or(MINIMUM_COMBINED_MASS);
86    trace_var!(minimum_combined_mass);
87    let maximum_combined_mass = self.maximum_combined_mass.unwrap_or(MAXIMUM_COMBINED_MASS);
88    trace_var!(maximum_combined_mass);
89    let minimum_individual_mass = self.minimum_individual_mass.unwrap_or(MINIMUM_INDIVIDUAL_MASS);
90    trace_var!(minimum_individual_mass);
91    let maximum_individual_mass = self.maximum_individual_mass.unwrap_or(MAXIMUM_INDIVIDUAL_MASS);
92    trace_var!(maximum_individual_mass);
93    let minimum_orbital_eccentricity = self
94      .minimum_orbital_eccentricity
95      .unwrap_or(MINIMUM_ORBITAL_ECCENTRICITY);
96    trace_var!(minimum_orbital_eccentricity);
97    let maximum_orbital_eccentricity = self
98      .maximum_orbital_eccentricity
99      .unwrap_or(MAXIMUM_ORBITAL_ECCENTRICITY);
100    trace_var!(maximum_orbital_eccentricity);
101    let minimum_average_separation = self.minimum_average_separation.unwrap_or(MINIMUM_AVERAGE_SEPARATION);
102    trace_var!(minimum_average_separation);
103    let maximum_average_separation = self.maximum_average_separation.unwrap_or(MAXIMUM_AVERAGE_SEPARATION);
104    trace_var!(maximum_average_separation);
105    let orbital_eccentricity = rng.gen_range(minimum_orbital_eccentricity..maximum_orbital_eccentricity);
106    trace_var!(orbital_eccentricity);
107    let average_separation = rng.gen_range(minimum_average_separation..maximum_average_separation);
108    trace_var!(average_separation);
109    let combined_mass;
110    let primary_mass;
111    let secondary_mass;
112    let mut primary_constraints;
113    let mut secondary_constraints;
114    if self.enforce_habitability {
115      let bare_minimum =
116        (1.1 * (4.0 * maximum_average_separation * (1.0 + orbital_eccentricity)).powf(2.0)).powf(1.0 / 4.0);
117      if minimum_combined_mass < bare_minimum {
118        minimum_combined_mass = 1.1 * bare_minimum;
119      }
120      primary_constraints = self.star_constraints.unwrap_or_else(StarConstraints::habitable);
121      secondary_constraints = self.star_constraints.unwrap_or_else(StarConstraints::habitable);
122    } else {
123      primary_constraints = self.star_constraints.unwrap_or_default();
124      secondary_constraints = self.star_constraints.unwrap_or_default();
125    }
126    let (primary, secondary) = {
127      combined_mass = rng.gen_range(minimum_combined_mass..maximum_combined_mass);
128      let half = combined_mass / 2.0;
129      let mut top = combined_mass - MINIMUM_HABITABLE_INDIVIDUAL_MASS;
130      if self.enforce_habitability && top > maximum_individual_mass {
131        top = maximum_individual_mass;
132      }
133      primary_mass = rng.gen_range(half..top);
134      secondary_mass = combined_mass - primary_mass;
135      primary_constraints.minimum_mass = Some(0.999 * primary_mass);
136      primary_constraints.maximum_mass = Some(1.001 * primary_mass);
137      secondary_constraints.minimum_mass = Some(0.999 * secondary_mass);
138      secondary_constraints.maximum_mass = Some(1.001 * secondary_mass);
139      let mut primary = primary_constraints.generate(rng)?;
140      let mut secondary = secondary_constraints.generate(rng)?;
141      let minimum_age = match self.enforce_habitability {
142        true => MINIMUM_HABITABLE_AGE,
143        false => 0.1 * primary.life_expectancy,
144      };
145      trace_var!(minimum_age);
146      let maximum_age = 0.9 * primary.life_expectancy;
147      trace_var!(maximum_age);
148      let current_age = rng.gen_range(minimum_age..maximum_age);
149      trace_var!(current_age);
150      primary.current_age = current_age;
151      secondary.current_age = current_age;
152      (primary, secondary)
153    };
154    trace_var!(primary);
155    trace_var!(secondary);
156    let result = CloseBinaryStar::from_stars(rng, primary, secondary, average_separation, orbital_eccentricity)?;
157    trace_var!(result);
158    trace_exit!();
159    Ok(result)
160  }
161}
162
163impl Default for Constraints {
164  /// No constraints, just let it all hang out.
165  #[named]
166  fn default() -> Self {
167    trace_enter!();
168    let minimum_combined_mass = Some(MINIMUM_COMBINED_MASS);
169    let maximum_combined_mass = Some(MAXIMUM_COMBINED_MASS);
170    let minimum_individual_mass = Some(MINIMUM_INDIVIDUAL_MASS);
171    let maximum_individual_mass = Some(MAXIMUM_INDIVIDUAL_MASS);
172    let minimum_average_separation = None;
173    let maximum_average_separation = None;
174    let minimum_orbital_eccentricity = Some(MINIMUM_ORBITAL_ECCENTRICITY);
175    let maximum_orbital_eccentricity = Some(MAXIMUM_ORBITAL_ECCENTRICITY);
176    let minimum_age = None;
177    let maximum_age = None;
178    let enforce_habitability = false;
179    let star_constraints = None;
180    let result = Self {
181      minimum_combined_mass,
182      maximum_combined_mass,
183      minimum_individual_mass,
184      maximum_individual_mass,
185      minimum_average_separation,
186      maximum_average_separation,
187      minimum_orbital_eccentricity,
188      maximum_orbital_eccentricity,
189      minimum_age,
190      maximum_age,
191      enforce_habitability,
192      star_constraints,
193    };
194    trace_var!(result);
195    trace_exit!();
196    result
197  }
198}
199
200#[cfg(test)]
201pub mod test {
202
203  use rand::prelude::*;
204
205  use super::*;
206  use crate::test::*;
207
208  #[named]
209  #[test]
210  pub fn test_default() -> Result<(), Error> {
211    init();
212    trace_enter!();
213    let mut rng = thread_rng();
214    trace_var!(rng);
215    let binary = &Constraints::default().generate(&mut rng)?;
216    trace_var!(binary);
217    print_var!(binary);
218    trace_exit!();
219    Ok(())
220  }
221
222  #[named]
223  #[test]
224  pub fn test_habitable() -> Result<(), Error> {
225    init();
226    trace_enter!();
227    let mut rng = thread_rng();
228    trace_var!(rng);
229    let binary = &Constraints::habitable().generate(&mut rng)?;
230    trace_var!(binary);
231    print_var!(binary);
232    trace_exit!();
233    Ok(())
234  }
235
236  #[named]
237  #[test]
238  pub fn test_default_bulk() -> Result<(), Error> {
239    init();
240    trace_enter!();
241    let mut rng = thread_rng();
242    trace_var!(rng);
243    let mut success = 0;
244    let trials = 1000;
245    let mut counter = 0;
246    loop {
247      match &Constraints::default().generate(&mut rng) {
248        Ok(_binary) => success += 1,
249        Err(error) => print_var!(error),
250      }
251      counter += 1;
252      if counter >= trials {
253        break;
254      }
255    }
256    print_var!(success);
257    trace_exit!();
258    Ok(())
259  }
260
261  #[named]
262  #[test]
263  pub fn test_habitable_bulk() -> Result<(), Error> {
264    init();
265    trace_enter!();
266    let mut rng = thread_rng();
267    trace_var!(rng);
268    let mut success = 0;
269    let trials = 1000;
270    let mut counter = 0;
271    loop {
272      match &Constraints::habitable().generate(&mut rng) {
273        Ok(_binary) => success += 1,
274        Err(error) => print_var!(error),
275      }
276      counter += 1;
277      if counter >= trials {
278        break;
279      }
280    }
281    print_var!(success);
282    assert_eq!(counter, trials);
283    trace_exit!();
284    Ok(())
285  }
286}