ballistics-engine 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7a0d1f4919b0dbd4941a2b98921abb22d3cd42e99c7c1a010901a05eb61af38
4
- data.tar.gz: 47dbe46168d35027c1b206bfc29e4769f0260f6e5d4710d339012cc59216dd00
3
+ metadata.gz: bf2213952856a92c87940abc9b7e23027d5ed76ea0663d71802f404cdcc6a9ad
4
+ data.tar.gz: c0b873d345e641ded5b8b989132154205092d4e70109624095b1b6e6a4727e6e
5
5
  SHA512:
6
- metadata.gz: c1ac430c7692c581438b9e67fac672866d35371ed442887049436ee2e4b4a95c2f457a43a0912fec8702cb112032a8b241ac787dcea426cd80e12178a11fc4ab
7
- data.tar.gz: e5a0a6fb34880d6f7d669b9df339bbdbb93af4c1515a533bf9c556f349fccae6f2ac079f7125e3ed92154f843969192aa9bef7fe952f7391fd2f3d08ee825950
6
+ metadata.gz: 9e52cc28510c5ff5bf87a8f9580551302405013bda69dad5fc0b7f505899bf8cebdf6007692b79db776a4f38a908d89dd0ee95b5f2e6e0065ba3ab597c5a6570
7
+ data.tar.gz: c941a4b498cea6e5e8867bc6c751d1282e46e07eaf2b813fc766c865adfd1c4f4cacbbb0b4670a1812d0908aa86821f776a677027d604f108ee8665b4824f4e7
data/Cargo.lock CHANGED
@@ -43,9 +43,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
43
43
 
44
44
  [[package]]
45
45
  name = "ballistics-engine"
46
- version = "0.48.0"
46
+ version = "0.50.0"
47
47
  source = "registry+https://github.com/rust-lang/crates.io-index"
48
- checksum = "9107a91d1664aa5cf4f56244ab5ce7baa07ed37a427bad2a335ad849b2cb8a03"
48
+ checksum = "d7f153b52bc99ee2e1303ba87e230435c4736f7464461559a2e9046b25503a71"
49
49
  dependencies = [
50
50
  "getrandom",
51
51
  "js-sys",
@@ -65,7 +65,7 @@ dependencies = [
65
65
 
66
66
  [[package]]
67
67
  name = "ballistics_engine_rb"
68
- version = "0.48.0"
68
+ version = "0.50.0"
69
69
  dependencies = [
70
70
  "ballistics-engine",
71
71
  "magnus",
data/Cargo.toml CHANGED
@@ -3,7 +3,7 @@
3
3
  # rake-compiler/rb-sys cargo-metadata lookup resolves (newer rb-sys does not
4
4
  # normalize hyphens). The published gem name is set in the gemspec, independent.
5
5
  name = "ballistics_engine_rb"
6
- version = "0.48.0"
6
+ version = "0.50.0"
7
7
  edition = "2021"
8
8
  authors = ["Alex Jokela <email@tinycomputers.io>"]
9
9
  description = "Ruby bindings for ballistics-engine"
@@ -18,7 +18,7 @@ crate-type = ["cdylib"]
18
18
  # `bridge` exposes ballistics_engine::bridge::bridge_call — the versioned JSON command
19
19
  # bridge that reaches every engine command from one string-in/string-out entry point.
20
20
  # It is pure serde/serde_json (already core engine deps), so it adds no new dependencies.
21
- ballistics-engine = { version = "0.48.0", default-features = false, features = ["bridge"] }
21
+ ballistics-engine = { version = "0.50.0", default-features = false, features = ["bridge"] }
22
22
  magnus = "0.8"
23
23
 
24
24
  [profile.release]
data/README.md CHANGED
@@ -51,8 +51,9 @@ result = BallisticsEngine.solve(
51
51
  "zero_distance_yards" => 100.0,
52
52
  "drag_model" => "G7", # G1 | G7 | G8
53
53
  "wind" => { "speed_mph" => 10.0, "direction_degrees" => 90.0 },
54
- "atmosphere" => { "temperature_f" => 59.0, "pressure_inhg" => 29.92,
55
- "humidity_percent" => 50.0, "altitude_feet" => 0.0 }
54
+ # Readings: pass the ones you have and leave the rest out (see "Atmosphere" below).
55
+ "atmosphere" => { "temperature_f" => 72.0, "pressure_inhg" => 29.31,
56
+ "humidity_percent" => 50.0, "altitude_feet" => 600.0 }
56
57
  )
57
58
 
58
59
  puts "Max range: #{result['max_range_yards'].round(1)} yd"
@@ -121,11 +122,16 @@ keys** in imperial units. Unknown keys are ignored; only the seven marked requir
121
122
  `twist_rate_inches`(10), `is_right_twist`(true), `muzzle_height_inches`, `target_height_inches`.
122
123
  - Wind: `wind` = `{speed_mph, direction_degrees}`; `wind_segments` = `[[speed_mph, angle_deg, until_yards], ...]`.
123
124
  - Atmosphere: `atmosphere` = `{temperature_f, pressure_inhg, humidity_percent, altitude_feet}`.
125
+ `pressure_inhg` is absolute station pressure, not a sea-level-corrected altimeter setting.
126
+ See "Atmosphere readings" below.
124
127
  - BC: `use_bc_segments`, `bc_segments_data` (see above), `bc_segments` = `[[mach, bc], ...]`.
125
128
  - Coriolis: `enable_coriolis`, `latitude_degrees`, `shot_direction_degrees`.
126
129
  - Physics flags: `enable_advanced_effects`, `enable_magnus`, `enable_aerodynamic_jump`,
127
130
  `use_enhanced_spin_drift`, `use_form_factor`, `use_cluster_bc`, `enable_pitch_damping`,
128
131
  `enable_precession_nutation`, `enable_wind_shear` (+ `wind_shear_model`), `use_rk4`, `use_adaptive_rk45`.
132
+ Leaving `wind_shear_model` out picks `"power_law"` (the CLI's default model) when
133
+ `enable_wind_shear` is true and `"none"` when it is false; a model you pass, `"none"`
134
+ included, is used exactly as given.
129
135
  - Powder: `use_powder_sensitivity`, `powder_temp_sensitivity`, `powder_temp_f`,
130
136
  `powder_temp_curve` = `[[temp_f, velocity_fps], ...]`, `powder_curve_temp_f`.
131
137
  - Sampling/solver: `enable_trajectory_sampling`, `sample_interval_yards`, `max_range_yards`, `time_step_seconds`.
@@ -135,6 +141,20 @@ keys** in imperial units. Unknown keys are ignored; only the seven marked requir
135
141
  y=drop, z=windage, in yards). Present when enabled: `sampled_points`, `aerodynamic_jump`,
136
142
  `angular_state`, `max_yaw_angle_rad`, `max_precession_angle_rad`, `min_pitch_damping`, `transonic_mach`.
137
143
 
144
+ #### Atmosphere readings
145
+
146
+ A `temperature_f` or `pressure_inhg` key in the `atmosphere` hash is a reading and flies
147
+ exactly as given, whatever its value and whatever the altitude. A real 59 °F or 29.92 inHg
148
+ at 5000 ft flies as 59 °F or 29.92 inHg. Leave a key out (or set it to `nil`) when you have
149
+ no reading. It then defaults to 59 °F / 29.92 inHg, and above 1 m of altitude it flies the
150
+ ICAO standard air at `altitude_feet` instead (about 41 °F and 24.9 inHg at 5000 ft). With no
151
+ `atmosphere` hash at all the shot is at sea level in standard air.
152
+
153
+ Before engine 0.49.0 the binding could not tell the two apart. At any altitude above 1 m, a
154
+ temperature within 0.1 °C of 15 °C (58.82 to 59.18 °F) or a pressure within 0.5 hPa of
155
+ 1013.25 hPa (about 29.906 to 29.936 inHg) was treated as omitted and replaced with standard
156
+ air, even when it was a real reading.
157
+
138
158
  ### `BallisticsEngine.bridge_call(request_json) => String`
139
159
 
140
160
  Raw pass-through to the engine's versioned JSON command bridge: one full request
@@ -152,8 +172,8 @@ def bridge(command, request)
152
172
  end
153
173
 
154
174
  bridge("meta.version", {})
155
- # => {"ok"=>true, "api_version"=>1, "engine_version"=>"0.39.0",
156
- # "command"=>"meta.version", "result"=>{"engine_version"=>"0.39.0"}}
175
+ # => {"ok"=>true, "api_version"=>1, "engine_version"=>"0.50.0",
176
+ # "command"=>"meta.version", "result"=>{"engine_version"=>"0.50.0"}}
157
177
 
158
178
  # Every command reachable in this build:
159
179
  bridge("meta.capabilities", {})["result"]["commands"]
@@ -194,8 +214,9 @@ what this build can actually run.
194
214
  ### `BallisticsEngine.calculate_zero_angle(hash) => hash`
195
215
 
196
216
  Same bullet keys as `solve`, plus **required** `target_distance_yards` (and optional
197
- `target_height_inches`, `wind`, `atmosphere`). Returns `zero_angle_radians`,
198
- `zero_angle_degrees`, `zero_angle_moa`. Raises on non-convergence.
217
+ `target_height_inches`, `wind`, `atmosphere`; atmosphere readings work as for `solve`).
218
+ Returns `zero_angle_radians`, `zero_angle_degrees`, `zero_angle_moa`. Raises on
219
+ non-convergence.
199
220
 
200
221
  ### `BallisticsEngine.monte_carlo(hash) => hash`
201
222
 
@@ -205,6 +226,8 @@ Bullet keys (optionally nested under `base_inputs`) plus std-dev params:
205
226
  `base_wind_speed_mph`, `base_wind_direction_degrees`, `hit_radius_inches`(≈11.8).
206
227
  Returns `ranges_yards`, `impact_velocities_fps`, `impact_positions`
207
228
  (`[{vertical_inches, lateral_inches}, ...]`), `hit_probability`, `num_simulations`.
229
+ It has no atmosphere input: every simulation flies sea-level standard air, and an
230
+ `atmosphere` hash is ignored.
208
231
  ## Development
209
232
 
210
233
  After checking out the repo, run `bundle install` to install dependencies.
@@ -4,7 +4,7 @@ require "ballistics_engine_rb"
4
4
 
5
5
  # Top-level module for the BallisticsEngine gem
6
6
  module BallisticsEngine
7
- VERSION = "0.48.0"
7
+ VERSION = "0.50.0"
8
8
 
9
9
  class Error < StandardError; end
10
10
  end
data/src/lib.rs CHANGED
@@ -1,7 +1,8 @@
1
1
  use magnus::{Error, RArray, RHash, function};
2
2
  use ballistics_engine::{
3
3
  AtmosphericConditions, BCSegmentData, BallisticInputs, DragModel, MonteCarloParams,
4
- TrajectorySolver, WindConditions, calculate_zero_angle_with_conditions, run_monte_carlo,
4
+ StationAtmospherePresence, TrajectorySolver, WindConditions,
5
+ calculate_zero_angle_with_station_presence, run_monte_carlo,
5
6
  };
6
7
  use ballistics_engine::wind::WindSegment;
7
8
 
@@ -59,25 +60,43 @@ fn build_wind(h: &RHash) -> Result<WindConditions, Error> {
59
60
  }
60
61
  }
61
62
 
62
- /// Build AtmosphericConditions from an optional `atmosphere` sub-hash. Note:
63
- /// AtmosphericConditions.humidity is PERCENT (0-100), pressure is hPa, temp is Celsius.
64
- fn build_atmosphere(h: &RHash) -> Result<AtmosphericConditions, Error> {
63
+ /// Build AtmosphericConditions from an optional `atmosphere` sub-hash, with which of its
64
+ /// temperature and pressure are readings. Note: AtmosphericConditions.humidity is PERCENT
65
+ /// (0-100), pressure is hPa, temp is Celsius.
66
+ ///
67
+ /// A `temperature_f` or `pressure_inhg` key that is present (and not nil) is a reading and
68
+ /// flies exactly as given, whatever its value and whatever the altitude. A key left out takes
69
+ /// the 59 °F / 29.92 inHg default and keeps the engine's legacy rule: above 1 m of altitude
70
+ /// the default becomes the ICAO standard air there, as an omitted value always has. With no
71
+ /// `atmosphere` hash nothing is a reading, at sea level.
72
+ fn build_atmosphere(
73
+ h: &RHash,
74
+ ) -> Result<(AtmosphericConditions, StationAtmospherePresence), Error> {
65
75
  if let Some(a) = h.lookup::<_, Option<RHash>>("atmosphere")? {
66
- let temp_f = opt_f64(&a, "temperature_f", 59.0)?;
67
- let pressure_inhg = opt_f64(&a, "pressure_inhg", 29.92)?;
68
- Ok(AtmosphericConditions {
69
- temperature: (temp_f - 32.0) * 5.0 / 9.0,
70
- pressure: pressure_inhg * INHG_TO_HPA,
71
- humidity: opt_f64(&a, "humidity_percent", 50.0)?,
72
- altitude: opt_f64(&a, "altitude_feet", 0.0)? * FPS_TO_MPS,
73
- })
76
+ let temp_f = a.lookup::<_, Option<f64>>("temperature_f")?;
77
+ let pressure_inhg = a.lookup::<_, Option<f64>>("pressure_inhg")?;
78
+ let presence = StationAtmospherePresence::new(temp_f.is_some(), pressure_inhg.is_some());
79
+ let temp_f = temp_f.unwrap_or(59.0);
80
+ let pressure_inhg = pressure_inhg.unwrap_or(29.92);
81
+ Ok((
82
+ AtmosphericConditions {
83
+ temperature: (temp_f - 32.0) * 5.0 / 9.0,
84
+ pressure: pressure_inhg * INHG_TO_HPA,
85
+ humidity: opt_f64(&a, "humidity_percent", 50.0)?,
86
+ altitude: opt_f64(&a, "altitude_feet", 0.0)? * FPS_TO_MPS,
87
+ },
88
+ presence,
89
+ ))
74
90
  } else {
75
- Ok(AtmosphericConditions {
76
- temperature: 15.0,
77
- pressure: 1013.25,
78
- humidity: 50.0,
79
- altitude: 0.0,
80
- })
91
+ Ok((
92
+ AtmosphericConditions {
93
+ temperature: 15.0,
94
+ pressure: 1013.25,
95
+ humidity: 50.0,
96
+ altitude: 0.0,
97
+ },
98
+ StationAtmospherePresence::NEITHER,
99
+ ))
81
100
  }
82
101
  }
83
102
 
@@ -149,6 +168,27 @@ fn extract_powder_curve(h: &RHash) -> Result<Option<Vec<(f64, f64)>>, Error> {
149
168
  }
150
169
  }
151
170
 
171
+ /// The model `enable_wind_shear` turns on when the hash names none: the CLI's default
172
+ /// `--wind-shear-model` and the profile the C ABI names, since neither lets a caller ask for
173
+ /// shear without getting one.
174
+ const DEFAULT_WIND_SHEAR_MODEL: &str = "power_law";
175
+
176
+ /// Resolve `wind_shear_model`. A model the hash names, `"none"` included, is kept exactly as
177
+ /// given. When the key is absent (or nil) the default follows the flag: `"power_law"` when
178
+ /// `enable_wind_shear` is set, `"none"` when it is not.
179
+ ///
180
+ /// The omitted model used to default to `"none"` whatever the flag said. `TrajectorySolver`,
181
+ /// which every hash entry point here reaches, read that as the power law until engine
182
+ /// 0.49.0; from 0.49.0 it reads `"none"` as no shear, so without this default a hash that
183
+ /// only set the flag would silently lose the shear it has always had.
184
+ fn resolve_wind_shear_model(h: &RHash, enable_wind_shear: bool) -> Result<String, Error> {
185
+ Ok(match h.lookup::<_, Option<String>>("wind_shear_model")? {
186
+ Some(model) => model,
187
+ None if enable_wind_shear => DEFAULT_WIND_SHEAR_MODEL.to_string(),
188
+ None => "none".to_string(),
189
+ })
190
+ }
191
+
152
192
  /// Build a fully-populated BallisticInputs from an imperial Ruby hash (imperial -> SI).
153
193
  /// Shared by solve, calculate_zero_angle, and monte_carlo.
154
194
  fn build_inputs(ruby: &magnus::Ruby, h: &RHash) -> Result<BallisticInputs, Error> {
@@ -157,6 +197,8 @@ fn build_inputs(ruby: &magnus::Ruby, h: &RHash) -> Result<BallisticInputs, Error
157
197
  let bullet_weight_grains: f64 = h.fetch("bullet_weight_grains")?;
158
198
 
159
199
  let bc_seg_data = extract_bc_segments_data(h)?;
200
+ let enable_wind_shear = opt_bool(h, "enable_wind_shear", false)?;
201
+ let wind_shear_model = resolve_wind_shear_model(h, enable_wind_shear)?;
160
202
 
161
203
  let mut inputs = BallisticInputs {
162
204
  bc_type: drag_model,
@@ -202,8 +244,8 @@ fn build_inputs(ruby: &magnus::Ruby, h: &RHash) -> Result<BallisticInputs, Error
202
244
  .lookup::<_, Option<f64>>("powder_curve_temp_f")?
203
245
  .map(|tf| (tf - 32.0) * 5.0 / 9.0),
204
246
  // wind shear
205
- enable_wind_shear: opt_bool(h, "enable_wind_shear", false)?,
206
- wind_shear_model: opt_str(h, "wind_shear_model", "none")?,
247
+ enable_wind_shear,
248
+ wind_shear_model,
207
249
  // sampling
208
250
  enable_trajectory_sampling: opt_bool(h, "enable_trajectory_sampling", false)?,
209
251
  // BC segments
@@ -238,9 +280,10 @@ fn build_inputs(ruby: &magnus::Ruby, h: &RHash) -> Result<BallisticInputs, Error
238
280
  fn solve_trajectory(ruby: &magnus::Ruby, h: RHash) -> Result<RHash, Error> {
239
281
  let inputs = build_inputs(ruby, &h)?;
240
282
  let wind = build_wind(&h)?;
241
- let atmosphere = build_atmosphere(&h)?;
283
+ let (atmosphere, presence) = build_atmosphere(&h)?;
242
284
 
243
- let mut solver = TrajectorySolver::new(inputs, wind, atmosphere);
285
+ let mut solver =
286
+ TrajectorySolver::new_with_station_presence(inputs, wind, atmosphere, presence);
244
287
  let segs = extract_wind_segments(&h)?;
245
288
  if !segs.is_empty() {
246
289
  solver.set_wind_segments(segs);
@@ -338,14 +381,15 @@ fn calculate_zero_angle(ruby: &magnus::Ruby, h: RHash) -> Result<RHash, Error> {
338
381
  let target_yd: f64 = h.fetch("target_distance_yards")?;
339
382
  let target_h_in: f64 = h.lookup2("target_height_inches", 0.0)?;
340
383
  let wind = build_wind(&h)?;
341
- let atmosphere = build_atmosphere(&h)?;
384
+ let (atmosphere, presence) = build_atmosphere(&h)?;
342
385
 
343
- let rad = calculate_zero_angle_with_conditions(
386
+ let rad = calculate_zero_angle_with_station_presence(
344
387
  inputs,
345
388
  target_yd * YARDS_TO_METERS,
346
389
  target_h_in * INCHES_TO_METERS,
347
390
  wind,
348
391
  atmosphere,
392
+ presence,
349
393
  )
350
394
  .map_err(|e| {
351
395
  Error::new(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ballistics-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.48.0
4
+ version: 0.50.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Jokela
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys