├── .gitignore ├── .editorconfig ├── itm ├── Cargo.toml ├── src │ ├── formulae.rs │ ├── climate.rs │ └── lib.rs └── LICENSE ├── Cargo.toml ├── .travis.yml ├── src └── lib.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.for 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.toml] 12 | indent_size = 2 13 | 14 | [*.md] 15 | indent_size = 3 16 | -------------------------------------------------------------------------------- /itm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "splash-longleyrice" 3 | edition = "2018" 4 | version = "0.1.0" 5 | 6 | authors = ["Félix Saparelli "] 7 | description = "Longley-Rice RF Signal Propagation model" 8 | license = "CC0-1.0" 9 | 10 | [dependencies] 11 | num-complex = "0.2.3" 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "splash" 3 | edition = "2018" 4 | version = "0.1.0" 5 | 6 | authors = ["Félix Saparelli "] 7 | description = "RF Signal Propagation analysis tool: like SPLAT! but more flexible" 8 | license = "CC-BY-NC-SA-4.0" 9 | 10 | [dependencies] 11 | gdal = "0.5.0" 12 | geo = "0.12.2" 13 | num-complex = "0.2.3" 14 | itm = { path = "./itm", package = "splash-longleyrice" } 15 | 16 | [workspace] 17 | members = ["itm"] 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: rust 3 | 4 | matrix: 5 | fast_finish: true 6 | include: 7 | - rust: nightly 8 | - rust: beta 9 | - rust: stable 10 | 11 | allow_failures: 12 | - rust: nightly 13 | 14 | before_install: 15 | - set -e 16 | - rustup self update 17 | - which cargo-audit || cargo install cargo-audit 18 | 19 | script: 20 | - cargo check 21 | - cargo test 22 | - cargo audit 23 | 24 | after_script: set +e 25 | 26 | cache: cargo 27 | before_cache: 28 | # Travis can't cache files that are not readable by "others" 29 | - chmod -R a+r $HOME/.cargo 30 | 31 | # notifications: 32 | # email: 33 | # on_success: never 34 | -------------------------------------------------------------------------------- /itm/src/formulae.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for ITM. 2 | //! 3 | //! These are miscellaneous functions that are functionally pure but also 4 | //! implement well-known algorithms and formulae. They might be reusable 5 | //! elsewhere and/or they might benefit from optimisation or being replaced by 6 | //! calls to more efficient or correct versions. 7 | 8 | /// Least-squares linear fit over evenly-spaced data between two points. 9 | /// 10 | /// Returns _Z₀_ and _Zn_ for the line _Y = Z₀ + Zn × X_. 11 | /// 12 | /// See ITM section `<53>`. 13 | pub fn least_squares_linear_fit(interval: f64, data: &[f64], points: (f64, f64)) -> (f64, f64) { 14 | let xn = data.len() as f64; 15 | let mut xa = fortran_dim(points.0 / interval, 0.0) as isize as f64; 16 | let mut xb = xn - (fortran_dim(xn, points.1 / interval) as isize as f64); 17 | 18 | if xb <= xa { 19 | xa = fortran_dim(xa, 1.0); 20 | xb = xn - fortran_dim(xn, xb + 1.0); 21 | } 22 | 23 | let mut ja = xa as usize; 24 | let jb = xb as usize; 25 | 26 | let n = jb - ja; 27 | xa = xb - xa; 28 | 29 | let mut xx = -0.5 * xa; 30 | xb += xx; 31 | 32 | let mut a = 0.5 * (data[ja] + data[jb]); 33 | let mut b = 0.5 * (data[ja] - data[jb]) * xx; 34 | 35 | for _ in 2..=n { 36 | ja += 1; 37 | xx += 1.0; 38 | a += data[ja]; 39 | b += data[ja] * xx; 40 | } 41 | 42 | a /= xa; 43 | b = b * 12.0 / ((xa * xa + 2.0) * xa); 44 | 45 | (a - b * xb, a + b * (xn - xb)) 46 | } 47 | 48 | /// Fortran-style DIM operation. 49 | pub fn fortran_dim(x: f64, y: f64) -> f64 { 50 | (x - y).max(0.0) 51 | } 52 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![cfg_attr(feature = "cargo-clippy", deny(clippy_pedantic))] 3 | 4 | use geo::Point; 5 | use itm::*; 6 | 7 | const EQUATORIAL_RADIUS: f64 = 6_378_137.0; 8 | const POLAR_RADIUS: f64 = 6_356_752.3; 9 | 10 | /// Computes the geocentric radius (in metres) at a given latitude (in degrees). 11 | pub fn local_radius(latitude: f64) -> f64 { 12 | let cos = latitude.cos(); 13 | let sin = latitude.sin(); 14 | 15 | let upper = (EQUATORIAL_RADIUS.powi(2) * cos).powi(2) + (POLAR_RADIUS.powi(2) * sin).powi(2); 16 | let lower = (EQUATORIAL_RADIUS * cos).powi(2) + (POLAR_RADIUS * sin).powi(2); 17 | 18 | (upper / lower).sqrt() 19 | } 20 | 21 | #[test] 22 | fn test_local_radius() { 23 | assert_eq!(local_radius(0.0), EQUATORIAL_RADIUS); 24 | assert_eq!(local_radius(12.3), 6376666.768840445); 25 | assert_eq!(local_radius(-35.273), 6368978.931744378); 26 | assert_eq!(local_radius(90.0), 6361074.591356493); 27 | assert_eq!(local_radius(-1.469167), 6356974.249836446); 28 | } 29 | 30 | /// Base site definition. 31 | #[derive(Clone, Debug)] 32 | pub struct Site { 33 | /// Where it is 34 | position: Point, 35 | 36 | /// How high above ground 37 | aboveground: f64, 38 | 39 | /// What it's called 40 | name: String, 41 | } 42 | 43 | /// An RF transmitter and its parameters. 44 | #[derive(Clone, Debug)] 45 | pub struct Transmitter { 46 | site: Site, 47 | 48 | /// Earth Dielectric Constant (Relative permittivity) 49 | dielectric: f64, 50 | 51 | /// Earth Conductivity (Siemens per metre) 52 | conductivity: f64, 53 | 54 | /// Atmospheric Bending Constant (N-units) 55 | bending: f64, 56 | 57 | /// Site frequency (MHz) 58 | frequency: f64, 59 | 60 | /// Radio climate 61 | climate: climate::Climate, 62 | 63 | /// Polarisation 64 | polarisation: Polarisation, 65 | 66 | /// Antenna pattern 67 | pattern: Vec>, 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Splash 2 | 3 | **[SPLAT!]** is a wonderful tool for RF analysis over terrain. Unfortunately, 4 | it only works with a few terrain sources and only uses one core for processing. 5 | 6 | [SPLAT!]: http://www.qsl.net/kd2bd/splat.html 7 | 8 | **Splash** is three things, in this order: 9 | 10 | 1. A learning project to teach myself about the maths and physics underlying 11 | the [Longley-Rice] Irregular Terrain Model (ITM) as I go about porting it to 12 | Rust. 13 | 2. A learning project to teach myself deep optimisation over modern hardware, 14 | using at least SIMD and hopefully GPU to dramatically improve modelisation 15 | performance. 16 | 3. A production-grade interface to the ITM, and eventually other models. 17 | 18 | [Longley-Rice]: https://en.wikipedia.org/wiki/Longley%E2%80%93Rice_model 19 | 20 | ## Porting goals 21 | 22 | - Not merely a port of the C++ port of the FORTRAN original, but a full 23 | refactor, with all functions and variables named sensibly. 24 | - Everything thoroughly documented inline, with the minor goal that one should 25 | be able to understand (at a high level) how the ITM works “simply” by 26 | reading Splash’s source. 27 | - 1950-era approximations replaced by exact versions for math functions where 28 | possible. 29 | - Entirely safe code. 30 | - Separation of preparation and execution routines. 31 | 32 | ## Research and papers 33 | 34 | ### Longley-Rice ITM 35 | 36 | - [The Irregular Terrain Model]: FORTRAN implementation with descriptions (NTIA 2002) 37 | - [Hufford 1995]: The (ITM) Algorithm, v1.2.2 38 | - [Hufford-Longley-Kissick 1982]: A Guide to the Use of the ITM 39 | - [McKenna 2016]: Propagation Measurement Workshop (Slides) 40 | 41 | ### Background 42 | 43 | - [Norton 1959]: Transmission Loss in Radio Propagation II 44 | - [Handbook on Ground Wave Propagation]: Edition of 2014 (ITU Radiocommunication Bureau) 45 | - [Phillips 2012]: Geostatistical Techniques for Practical Wireless Network Coverage Mapping 46 | 47 | ### Parallel ITM 48 | 49 | - [Song 2011]: Parallel Implementation of the Irregular Terrain Model (ITM) for Radio Transmission Loss Prediction Using GPU and Cell BE Processors 50 | - [Musselman 2013]: An OpenCL Implementation of the Irregular Terrain with Obstructions Model (ITWOM) 51 | 52 | ### Accuracy studies 53 | 54 | - [Sun 2015]: Propagation Path Loss Models for 5G Urban Micro-and Macro-Cellular Scenarios 55 | - [Sun 2016]: Investigation of Prediction Accuracy, Sensitivity, and Parameter Stability of Large-Scale Propagation Path Loss Models for 5G Wireless Communications 56 | - [Abhayawardhana 2005]: Comparison of Empirical Propagation Path Loss Models for Fixed Wireless Access Systems 57 | 58 | ### Other models or improvements 59 | 60 | - [El-Sallabi 2011]: Terrain Partial Obstruction LOS Path Loss Model for Rural Environments 61 | - [Phillips 2012]: Geostatistical Techniques for Practical Wireless Network Coverage Mapping 62 | - [MacCartney 2017]: Rural Macrocell Path Loss Models for Millimeter Wave Wireless Communications 63 | 64 | [The Irregular Terrain Model]: https://www.its.bldrdoc.gov/media/50674/itm.pdf 65 | [Hufford 1995]: https://www.its.bldrdoc.gov/media/50676/itm_alg.pdf 66 | [Hufford-Longley-Kissick 1982]: https://www.ntia.doc.gov/files/ntia/publications/ntia_82-100_20121129145031_555510.pdf 67 | [McKenna 2016]: https://www.its.bldrdoc.gov/resources/workshops/propagation-measurement-workshops-webinars.aspx 68 | [Norton 1959]: https://nvlpubs.nist.gov/nistpubs/Legacy/TN/nbstechnicalnote12.pdf 69 | [Handbook on Ground Wave Propagation]: https://www.itu.int/dms_pub/itu-r/opb/hdb/R-HDB-59-2014-PDF-E.pdf 70 | [Phillips 2012]: https://core.ac.uk/display/54849067 71 | [Song 2011]: https://ieeexplore.ieee.org/document/5680900/ 72 | [Musselman 2013]: https://github.com/amusselm/Parallel-LRP/blob/master/documents/report.pdf 73 | [Sun 2016]: https://arxiv.org/abs/1603.04404 74 | [Sun 2015]: https://arxiv.org/abs/1511.07311 75 | [Abhayawardhana 2005]: https://ieeexplore.ieee.org/document/1543252 76 | [El-Sallabi 2011]: https://ieeexplore.ieee.org/document/5701648 77 | [MacCartney 2017]: https://ieeexplore.ieee.org/document/7914696 78 | -------------------------------------------------------------------------------- /itm/LICENSE: -------------------------------------------------------------------------------- 1 | Public Domain | Félix Saparelli 2 | Any action relating to this project may only be brought in New Zealand. 3 | 4 | 5 | Creative Commons CC0 1.0 Universal 6 | 7 | Statement of Purpose 8 | 9 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 10 | 11 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 12 | 13 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 14 | 15 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 16 | 17 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 18 | 19 | ii. moral rights retained by the original author(s) and/or performer(s); 20 | 21 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 22 | 23 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 24 | 25 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 26 | 27 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 28 | 29 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 30 | 31 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 32 | 33 | 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 34 | 35 | 4. Limitations and Disclaimers. 36 | 37 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 38 | 39 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 40 | 41 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 42 | 43 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 44 | -------------------------------------------------------------------------------- /itm/src/climate.rs: -------------------------------------------------------------------------------- 1 | // #![deny(missing_docs)] 2 | 3 | //! Climate enum, constants, and Long Term Fading calculations. 4 | //! 5 | //! TODO: LTF explainer. 6 | 7 | /// Radio-climate regions. 8 | /// 9 | /// TODO: explainer. 10 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 11 | pub enum Climate { 12 | Equatorial, 13 | ContinentalSubtropical, 14 | MaritimeSubtropical, 15 | Desert, 16 | ContinentalTemperate, 17 | MaritimeTemperateOverLand, 18 | MaritimeTemperateOverSea, 19 | } 20 | 21 | impl Default for Climate { 22 | fn default() -> Self { 23 | Climate::ContinentalTemperate 24 | } 25 | } 26 | 27 | /// Climate constants for Long Term Fading calculations. 28 | /// 29 | /// Instead of just having a bunch of arrays to hold these, they are grouped 30 | /// in meaningful bunches (although the names remain inscrutable, their usage 31 | /// clearly indicates this arrangement holds some kind of meaning), and one 32 | /// struct holds all the constants for one climate. 33 | /// 34 | /// To get the constants for a climate, use: 35 | /// 36 | /// ``` 37 | /// # use splash::itm::climate::{Climate, ClimateConstants}; 38 | /// # let climate = Climate::default(); 39 | /// let cc: ClimateConstants = climate.into(); 40 | /// ``` 41 | #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] 42 | pub struct ClimateConstants { 43 | pub name: Climate, 44 | 45 | pub bv: (f64, f64), 46 | pub xv: (f64, f64, f64), 47 | pub bsm: (f64, f64), 48 | pub xsm: (f64, f64, f64), 49 | pub bsp: (f64, f64), 50 | pub xsp: (f64, f64, f64), 51 | 52 | /// CD (Table 5.1 of T.A.) 53 | pub cd: f64, 54 | /// ZD (Table 5.1 of T.A.) 55 | pub zd: f64, 56 | 57 | pub bfm: (f64, f64, f64), 58 | pub bfp: (f64, f64, f64), 59 | } 60 | 61 | impl ClimateConstants { 62 | /// Creates from contants. 63 | fn new( 64 | name: Climate, 65 | bv: (f64, f64), 66 | xv: (f64, f64, f64), 67 | bsm: (f64, f64), 68 | xsm: (f64, f64, f64), 69 | bsp: (f64, f64), 70 | xsp: (f64, f64, f64), 71 | cd: f64, 72 | zd: f64, 73 | bfm: (f64, f64, f64), 74 | bfp: (f64, f64, f64), 75 | ) -> Self { 76 | Self { 77 | name, 78 | bv, 79 | xv, 80 | bsm, 81 | xsm, 82 | bsp, 83 | xsp, 84 | cd, 85 | zd, 86 | bfm, 87 | bfp, 88 | } 89 | } 90 | 91 | /// Computes long-term fading reference values from climate constants. 92 | pub fn reference_values(self, de: f64, wn: f64) -> (f64, f64, f64, f64, f64) { 93 | let q = (0.133 * wn).ln(); 94 | let gm = self.bfm.0 + self.bfm.1 / ((self.bfm.2 * q).powi(2) + 1.0); 95 | let gp = self.bfp.0 + self.bfp.1 / ((self.bfp.2 * q).powi(2) + 1.0); 96 | 97 | let vmd = Self::curve(self.bv, self.xv, de); 98 | let sgtm = Self::curve(self.bsm, self.xsm, de) * gm; 99 | let sgtp = Self::curve(self.bsp, self.xsp, de) * gp; 100 | 101 | let sgtd = sgtp * self.cd; 102 | let tgtd = (sgtp - sgtd) * self.zd; 103 | 104 | (vmd, sgtm, sgtp, sgtd, tgtd) 105 | } 106 | 107 | /// Long-term fading climate curves. 108 | /// 109 | /// This function's only reference is in the FORTRAN source. No comment is given 110 | /// as to how it was derived, and whether the figure in the research are from 111 | /// this function, or whether the function is fit from the figure. 112 | /// 113 | /// The figure is available in [Technical Note 101 Volume I][TN101-I] and 114 | /// [Volume II][TN101-II], sections 10 and III respectively. 115 | /// 116 | /// For more background details see the `avar` function documentation. 117 | /// 118 | /// [TN101-I]: https://www.its.bldrdoc.gov/publications/2726.aspx 119 | /// [TN101-II]: https://www.its.bldrdoc.gov/publications/2727.aspx 120 | pub fn curve(b: (f64, f64), x: (f64, f64, f64), de: f64) -> f64 { 121 | (b.0 + b.1 / (1.0 + ((de - x.1) / x.2).powi(2))) * (de / x.0).powi(2) 122 | / (1.0 + (de / x.0).powi(2)) 123 | } 124 | } 125 | 126 | impl From for ClimateConstants { 127 | fn from(climate: Climate) -> Self { 128 | match climate { 129 | name @ Climate::Equatorial => Self::new( 130 | name, 131 | (-9.67, 12.7), 132 | (144.9e3, 190.3e3, 133.8e3), 133 | (2.13, 159.5), 134 | (762.2e3, 123.6e3, 94.5e3), 135 | (2.11, 102.3), 136 | (636.9e3, 134.8e3, 95.6e3), 137 | 1.224, 138 | 1.282, 139 | (1.0, 0.0, 0.0), 140 | (1.0, 0.0, 0.0), 141 | ), 142 | name @ Climate::ContinentalSubtropical => Self::new( 143 | name, 144 | (-0.62, 9.19), 145 | (228.9e3, 205.2e3, 143.6e3), 146 | (2.66, 7.67), 147 | (100.4e3, 172.5e3, 136.4e3), 148 | (6.87, 15.53), 149 | (138.7e3, 143.7e3, 98.6e3), 150 | 0.801, 151 | 2.161, 152 | (1.0, 0.0, 0.0), 153 | (0.93, 0.31, 2.00), 154 | ), 155 | name @ Climate::MaritimeSubtropical => Self::new( 156 | name, 157 | (1.26, 15.5), 158 | (262.6e3, 185.2e3, 99.8e3), 159 | (6.11, 6.65), 160 | (138.2e3, 242.2e3, 178.6e3), 161 | (10.08, 9.60), 162 | (165.3e3, 225.7e3, 129.7e3), 163 | 1.380, 164 | 1.282, 165 | (1.0, 0.0, 0.0), 166 | (1.0, 0.0, 0.0), 167 | ), 168 | name @ Climate::Desert => Self::new( 169 | name, 170 | (-9.21, 9.05), 171 | (84.1e3, 101.1e3, 98.6e3), 172 | (1.98, 13.11), 173 | (139.1e3, 132.7e3, 193.5e3), 174 | (3.68, 159.3), 175 | (464.4e3, 93.1e3, 94.2e3), 176 | 1.000, 177 | 20.0, 178 | (1.0, 0.0, 0.0), 179 | (0.93, 0.19, 1.79), 180 | ), 181 | name @ Climate::ContinentalTemperate => Self::new( 182 | name, 183 | (-0.62, 9.19), 184 | (228.9e3, 205.2e3, 143.6e3), 185 | (2.68, 7.16), 186 | (93.7e3, 186.8e3, 133.5e3), 187 | (4.75, 8.12), 188 | (93.2e3, 135.9e3, 113.4e3), 189 | 1.224, 190 | 1.282, 191 | (0.92, 0.25, 1.77), 192 | (0.93, 0.31, 2.00), 193 | ), 194 | name @ Climate::MaritimeTemperateOverLand => Self::new( 195 | name, 196 | (-0.39, 2.86), 197 | (141.7e3, 315.9e3, 167.4e3), 198 | (6.86, 10.38), 199 | (187.8e3, 169.6e3, 108.9e3), 200 | (8.58, 13.97), 201 | (216.0e3, 152.0e3, 122.7e3), 202 | 1.518, 203 | 1.282, 204 | (1.0, 0.0, 0.0), 205 | (1.0, 0.0, 0.0), 206 | ), 207 | name @ Climate::MaritimeTemperateOverSea => Self::new( 208 | name, 209 | (3.15, 857.9), 210 | (2222.0e3, 164.8e3, 116.3e3), 211 | (8.51, 169.8), 212 | (609.8e3, 119.9e3, 106.6e3), 213 | (8.43, 8.19), 214 | (136.2e3, 188.5e3, 122.9e3), 215 | 1.518, 216 | 1.282, 217 | (1.0, 0.0, 0.0), 218 | (1.0, 0.0, 0.0), 219 | ), 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "autocfg" 5 | version = "0.1.6" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "backtrace" 10 | version = "0.3.9" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | dependencies = [ 13 | "backtrace-sys 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 18 | ] 19 | 20 | [[package]] 21 | name = "backtrace-sys" 22 | version = "0.1.23" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | dependencies = [ 25 | "cc 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 27 | ] 28 | 29 | [[package]] 30 | name = "cc" 31 | version = "1.0.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "cfg-if" 36 | version = "0.1.4" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | 39 | [[package]] 40 | name = "failure" 41 | version = "0.1.5" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | dependencies = [ 44 | "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 45 | "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 46 | ] 47 | 48 | [[package]] 49 | name = "failure_derive" 50 | version = "0.1.5" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | dependencies = [ 53 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 54 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 55 | "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", 56 | "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", 57 | ] 58 | 59 | [[package]] 60 | name = "gdal" 61 | version = "0.5.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | dependencies = [ 64 | "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 65 | "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 66 | "gdal-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 67 | "geo-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 70 | ] 71 | 72 | [[package]] 73 | name = "gdal-sys" 74 | version = "0.2.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | dependencies = [ 77 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 78 | "pkg-config 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", 79 | ] 80 | 81 | [[package]] 82 | name = "geo" 83 | version = "0.12.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | dependencies = [ 86 | "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 87 | "geo-types 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 89 | "rstar 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 90 | ] 91 | 92 | [[package]] 93 | name = "geo-types" 94 | version = "0.3.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | dependencies = [ 97 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 98 | ] 99 | 100 | [[package]] 101 | name = "geo-types" 102 | version = "0.4.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | dependencies = [ 105 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 106 | "rstar 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 107 | ] 108 | 109 | [[package]] 110 | name = "libc" 111 | version = "0.2.42" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | 114 | [[package]] 115 | name = "num-complex" 116 | version = "0.2.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | dependencies = [ 119 | "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 120 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 121 | ] 122 | 123 | [[package]] 124 | name = "num-traits" 125 | version = "0.2.8" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | dependencies = [ 128 | "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 129 | ] 130 | 131 | [[package]] 132 | name = "num_cpus" 133 | version = "1.10.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | dependencies = [ 136 | "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", 137 | ] 138 | 139 | [[package]] 140 | name = "pdqselect" 141 | version = "0.1.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | 144 | [[package]] 145 | name = "pkg-config" 146 | version = "0.3.12" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | 149 | [[package]] 150 | name = "proc-macro2" 151 | version = "0.4.30" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | dependencies = [ 154 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 155 | ] 156 | 157 | [[package]] 158 | name = "quote" 159 | version = "0.6.13" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | dependencies = [ 162 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 163 | ] 164 | 165 | [[package]] 166 | name = "rstar" 167 | version = "0.2.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | dependencies = [ 170 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 171 | "pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 172 | "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 173 | ] 174 | 175 | [[package]] 176 | name = "rustc-demangle" 177 | version = "0.1.9" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | 180 | [[package]] 181 | name = "splash" 182 | version = "0.1.0" 183 | dependencies = [ 184 | "gdal 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 185 | "geo 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", 186 | "num-complex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 187 | "splash-longleyrice 0.1.0", 188 | ] 189 | 190 | [[package]] 191 | name = "splash-longleyrice" 192 | version = "0.1.0" 193 | dependencies = [ 194 | "num-complex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 195 | ] 196 | 197 | [[package]] 198 | name = "syn" 199 | version = "0.15.44" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | dependencies = [ 202 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 203 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 204 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 205 | ] 206 | 207 | [[package]] 208 | name = "synstructure" 209 | version = "0.10.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | dependencies = [ 212 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 213 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 214 | "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", 215 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 216 | ] 217 | 218 | [[package]] 219 | name = "threadpool" 220 | version = "1.7.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | dependencies = [ 223 | "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", 224 | ] 225 | 226 | [[package]] 227 | name = "unicode-xid" 228 | version = "0.1.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | 231 | [[package]] 232 | name = "winapi" 233 | version = "0.3.5" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | dependencies = [ 236 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 237 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 238 | ] 239 | 240 | [[package]] 241 | name = "winapi-i686-pc-windows-gnu" 242 | version = "0.4.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | 245 | [[package]] 246 | name = "winapi-x86_64-pc-windows-gnu" 247 | version = "0.4.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | 250 | [metadata] 251 | "checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" 252 | "checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" 253 | "checksum backtrace-sys 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "bff67d0c06556c0b8e6b5f090f0eac52d950d9dfd1d35ba04e4ca3543eaf6a7e" 254 | "checksum cc 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "2119ea4867bd2b8ed3aecab467709720b2d55b1bcfe09f772fd68066eaf15275" 255 | "checksum cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efe5c877e17a9c717a0bf3613b2709f723202c4e4675cc8f12926ded29bcb17e" 256 | "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" 257 | "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" 258 | "checksum gdal 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9ea96babfd4d127c67d4ce010ba474c896cd4edde13030632b9b9b8928a2a6f8" 259 | "checksum gdal-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f14384767373cfd3f2055aeac829d8cb146e80f73bb70ddaa000b9efa51f0979" 260 | "checksum geo 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "89ce8faa25a6f5ce8ea98faa95247d66377a843408eb4418332ec37de96850b0" 261 | "checksum geo-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05350aaba2e842c89fc8e1b2e74eefded6f2abe6ce5e0f5b9037c4b71c9b3598" 262 | "checksum geo-types 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "866e8f6dbd2218b05ea8a25daa1bfac32b0515fe7e0a37cb6a7b9ed0ed82a07e" 263 | "checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" 264 | "checksum num-complex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fcb0cf31fb3ff77e6d2a6ebd6800df7fdcd106f2ad89113c9130bcd07f93dffc" 265 | "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" 266 | "checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" 267 | "checksum pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" 268 | "checksum pkg-config 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)" = "6a52e4dbc8354505ee07e484ab07127e06d87ca6fa7f0a516a2b294e5ad5ad16" 269 | "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 270 | "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 271 | "checksum rstar 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "120bfe4837befb82c5a637a5a8c490a27d25524ac19fffec5b4e555ca6e36ee8" 272 | "checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" 273 | "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 274 | "checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" 275 | "checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" 276 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 277 | "checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" 278 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 279 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 280 | -------------------------------------------------------------------------------- /itm/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Longley-Rice Irregular Terrain Model for RF. 2 | //! 3 | //! This module was initially derived from a C++ translation of the original 4 | //! FORTRAN implementation of the ITM, the [Irregular Terrain Model][ITM68], 5 | //! also known as Longley-Rice, an empirical RF propagation model developed at 6 | //! the U.S. National Telecommunications and Information Administration around 7 | //! the 1960s by Anita Longley and Phil Rice. 8 | //! 9 | //! While the initial reconstruction was done referring to the above-mentioned 10 | //! source and its adaptation in [SPLAT!], all variables and function names were 11 | //! still unscrutable, as was the general operation of the model. Thus, the code 12 | //! was then cross-referenced back to the [LaTeX documentation][ITM122] of the 13 | //! ITM version 1.2.2 (sections within that document are referenced with `` 14 | //! in the code), and made more legible and understandable by deriving 15 | //! meaningful names and incorporating documentation into this source by 16 | //! referring back to George Hufford's 1999 memo describing 17 | //! “[The Algorithm][GH1999]” (referenced as T.A. in the code). 18 | //! 19 | //! Note that the structure of the module is quite different from the above 20 | //! implementations, making it more idiomatic to Rust and allowing more uses. 21 | //! Remarkably, we are much friendlier to concurrent computes. 22 | //! 23 | //! This implementation is released in the Public Domain, although note that the 24 | //! NTIA requests any use of the ITM is properly credited. 25 | //! 26 | //! [GH1999]: https://www.its.bldrdoc.gov/media/50676/itm_alg.pdf 27 | //! [ITM122]: https://www.its.bldrdoc.gov/media/50674/itm.pdf 28 | //! [ITM68]: https://www.its.bldrdoc.gov/resources/radio-propagation-software/itm/itm.aspx 29 | //! [SPLAT!]: http://www.qsl.net/kd2bd/splat.html 30 | 31 | #![forbid(unsafe_code)] 32 | #![deny( 33 | clippy::option_unwrap_used, 34 | clippy::result_unwrap_used 35 | )] 36 | 37 | use climate::{Climate, ClimateConstants}; 38 | use formulae::*; 39 | use num_complex::Complex64; 40 | 41 | pub mod climate; 42 | pub mod formulae; 43 | 44 | /// Propagation model instance. 45 | /// 46 | /// Holds all state related to one instance of the Irregular Terrain Model, for 47 | /// one particular transmitter, elevation profile from the transmitter outwards, 48 | /// and set of options. 49 | /// 50 | /// A `Model` instance can be used to query propagation at any distance from the 51 | /// transmitter along the loaded elevation profile. All preliminary work is done 52 | /// on initialisation: propagation queries only need immutable access and can 53 | /// therefore be done concurrently. 54 | #[derive(Clone, Debug, PartialEq)] 55 | pub struct Model<'a> { 56 | length: f64, // 57 | elevations: &'a Vec, 58 | heights: (f64, f64), // 59 | settings: &'a Settings, 60 | computed: Computed, 61 | cached: Cached, 62 | } 63 | 64 | /// Computed parameters of the model. 65 | /// 66 | /// These are computed from the input settings and elevation profile. 67 | /// 68 | /// In the source, `` indicate original variable names, such that one 69 | /// may cross-reference these back to the memos and other implementations. 70 | /// 71 | /// An exception is ``, here named `interval`. In the original, that 72 | /// parameter was an input, and the total _length_ of the profile was computed. 73 | /// That, along with the name, was confusing and possibly inflexible. Thus this 74 | /// bit of logic is inverted. 75 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 76 | pub struct Computed { 77 | /// [Wave number] of the carrier/central frequency (in radians per unit distance). 78 | /// 79 | /// [Wave number]: https://en.wikipedia.org/wiki/Wavenumber 80 | pub wave_number: f64, // 81 | 82 | /// General elevation: the mean elevation of the modeled system. 83 | pub general_elevation: f64, // 84 | 85 | /// Earth's effective curvature at the system's elevation. 86 | pub effective_curvature: f64, // 87 | 88 | /// Effective surface refractivity at the system's elevation. 89 | pub effective_refractivity: f64, // 90 | 91 | /// Surface transfer impedance to the ground. 92 | pub transfer_impedance: Complex64, // 93 | 94 | /// Interval distance between each elevation. 95 | pub interval: f64, // 96 | 97 | /// Elevation angles of the horizons from each terminal at the heights of 98 | /// the antennas, in radians. 99 | pub elevation_angles: (f64, f64), // 100 | 101 | /// Distances from each terminal to its radio horizon. 102 | pub horizon_distances: (f64, f64), //
103 | 104 | /// Terminal effective heights: adjusted against horizons (or obstructions). 105 | pub effective_heights: (f64, f64), // 106 | 107 | /// The interdecile range of elevations between the antennas (delta H). 108 | pub terrain_irregularity: f64, // 109 | } 110 | 111 | #[derive(Clone, Copy, Debug, PartialEq)] 112 | pub struct Cached { 113 | pub lvar: isize, 114 | pub variability_mode: isize, // 115 | 116 | pub dmin: f64, 117 | pub xae: f64, 118 | pub wlos: bool, 119 | pub wscat: bool, 120 | 121 | pub ad: f64, 122 | pub rr: f64, 123 | pub etq: f64, 124 | pub h0s: f64, 125 | } 126 | 127 | impl Default for Cached { 128 | fn default() -> Self { 129 | Self { 130 | lvar: 5, 131 | variability_mode: 12, 132 | 133 | dmin: 0.0, 134 | xae: 0.0, 135 | wlos: false, 136 | wscat: false, 137 | 138 | ad: 0.0, 139 | rr: 0.0, 140 | etq: 0.0, 141 | h0s: 0.0, 142 | } 143 | } 144 | } 145 | 146 | /// The polarisation of the radio wave. 147 | #[allow(missing_docs)] 148 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 149 | pub enum Polarisation { 150 | Horizontal, 151 | Vertical, 152 | } 153 | 154 | impl Default for Polarisation { 155 | fn default() -> Self { 156 | Polarisation::Horizontal 157 | } 158 | } 159 | 160 | /// The carrier or central frequency (in MHz). 161 | /// 162 | /// This is pre-computed (in the paper) assuming a speed of light **in air** of 163 | /// 299.7 metres per microsecond. See Fig/1.1 in T.A. 164 | const CARRIER_FREQUENCY: f64 = 47.7; 165 | 166 | impl<'a> Model<'a> { 167 | /// Creates a new instance for a profile. 168 | /// 169 | /// - `length` and `elevations` are the terrain input. We assume elevations 170 | /// are along a line of length `length` metres, spaced equidistantly, and 171 | /// that the transmitter is at distance zero. 172 | /// 173 | /// - `heights` is a tuple of the _height above ground_ in metres of the 174 | /// terminals (antennas), transmitter then receiver. 175 | /// 176 | /// - `settings` is an instance of the `Settings` struct, containing 177 | /// atmospheric, surface, radio, and statistical parameter values. 178 | /// 179 | /// Upon construction, we prepare all values needed for the individual 180 | /// calculations, perform bounds checking, and decide on the mode or modes 181 | /// of the model. 182 | /// 183 | /// The ITM has three different modes depending on the characteristics and 184 | /// distance modeled. We assume that all points within the given profile 185 | /// will be queried, so if the inputs span two or more modes, we prepare for 186 | /// each of those. 187 | /// 188 | /// - If the _distance to site_ is less than line of sight, then path loss 189 | /// is found from the Line-Of-Sight submodel, from optic horizon and the 190 | /// two-ray model. 191 | /// 192 | /// - If the _distance to site_ is larger than that, but still within the 193 | /// radio horizon, then the Diffraction submodel is used, considering the 194 | /// curvature of the Earth and knife-edge mechanisms. 195 | /// 196 | /// - If the _distance to site_ is beyond the radio horizon, the Scatter 197 | /// submodel is used, which computes constants for a linear relationship. 198 | /// 199 | /// After the attenuation is found, atmospheric and climate conditions are 200 | /// brought back to the global measurements made by the ITM team around the 201 | /// world and they are used to perform statistical quantile variability 202 | /// modelling given two input settings: % of time, and % of situations. 203 | pub fn new( 204 | length: f64, 205 | elevations: &'a Vec, 206 | heights: (f64, f64), 207 | settings: &'a Settings, 208 | ) -> Result { 209 | if elevations.len() < 3 { 210 | return Err("elevations should have more than 2 points".into()); 211 | } 212 | 213 | if heights.0 < 0.0 || heights.1 < 0.0 { 214 | return Err("heights may not be negative".into()); 215 | } 216 | 217 | let cached = Cached::default(); 218 | let computed = Computed::default(); 219 | let mut model = Self { 220 | length, 221 | elevations, 222 | heights, 223 | settings, 224 | computed, 225 | cached, 226 | }; 227 | 228 | model.prepare_environment(); 229 | model.find_horizons(); 230 | // mdvar, lvar ??? 231 | 232 | let xl = model.get_xl(); 233 | model.computed.terrain_irregularity = d1thx(model.computed.interval, model.elevations, xl); 234 | 235 | model.adjust_horizons(xl); 236 | 237 | // lrprop(0.0, prop, propa); 238 | 239 | // mdvar = mode of variability 240 | 241 | Ok(model) 242 | } 243 | 244 | /// Various basic computed parameters. 245 | /// 246 | /// See ITM section `<41>`. 247 | fn prepare_environment(&mut self) { 248 | // Fig/1.1 in T.A. 249 | self.computed.wave_number = self.settings.frequency / CARRIER_FREQUENCY; 250 | 251 | let sum: f64 = self.elevations.iter().sum(); 252 | self.computed.general_elevation = sum / (self.elevations.len() as f64); 253 | 254 | self.computed.effective_refractivity = self.settings.surface_refractivity; 255 | if self.computed.general_elevation != 0.0 { 256 | // Fig/1.2 in T.A. 257 | self.computed.effective_refractivity *= 258 | (-self.computed.general_elevation / 9460.0).exp(); 259 | } 260 | 261 | // Fig/1.3 in T.A. 262 | self.computed.effective_curvature = 263 | 157e-9 * (1.0 - 0.04665 * (self.computed.effective_refractivity / 179.3).exp()); 264 | 265 | // Fig/1.5 in T.A. 266 | let complex_relative_permittivity = Complex64::new( 267 | self.settings.permittivity, 268 | 376.62 * self.settings.conductivity / self.computed.wave_number, 269 | ); 270 | 271 | // Fig/1.4 in T.A. 272 | self.computed.transfer_impedance = (complex_relative_permittivity - 1.0).sqrt(); 273 | if self.settings.polarisation == Polarisation::Vertical { 274 | self.computed.transfer_impedance /= complex_relative_permittivity; 275 | } 276 | } 277 | 278 | /// Use the elevation profile to find the two horizons. 279 | /// 280 | /// See ITM section `<47>`. 281 | fn find_horizons(&mut self) { 282 | let len = self.elevations.len(); 283 | let interval = self.length / (len - 1) as f64; 284 | self.computed.interval = interval; // see note in Computed 285 | 286 | // absolute heights of terminals 287 | let tx_z = self.elevations[0] + self.heights.0; 288 | let rx_z = self.elevations[len - 1] + self.heights.1; 289 | 290 | let half_curve = self.computed.effective_curvature / 2.0; 291 | let half_length = half_curve * self.length; 292 | 293 | let vertical_delta = (rx_z - tx_z) / self.length; 294 | let mut angle_tx = vertical_delta - half_length; 295 | let mut angle_rx = -vertical_delta - half_length; 296 | 297 | let mut dist_tx = self.length; 298 | let mut dist_rx = self.length; 299 | 300 | let mut along_tx = 0.0; 301 | let mut along_rx = self.length; 302 | 303 | let mut wq = true; 304 | 305 | // We advance along the elevation profile looking both from the TX and 306 | // from the RX at the same time. As we go we adjust the elevations to 307 | // the Earth's curvature. 308 | 309 | for i in 1..len { 310 | along_tx += interval; 311 | along_rx -= interval; 312 | 313 | let tx_adj = half_curve * along_tx + angle_tx; 314 | let tx_delta = self.elevations[i] - tx_adj * along_tx - tx_z; 315 | 316 | if tx_delta > 0.0 { 317 | angle_tx += half_length / along_tx; 318 | dist_tx = along_tx; 319 | wq = false; 320 | } 321 | 322 | if wq { 323 | continue; 324 | } 325 | 326 | let rx_adj = half_curve * along_rx + angle_rx; 327 | let rx_delta = self.elevations[i] - rx_adj * along_rx - rx_z; 328 | 329 | if rx_delta > 0.0 { 330 | angle_rx += half_length / along_rx; 331 | dist_rx = along_rx; 332 | } 333 | } 334 | 335 | self.computed.elevation_angles = (angle_tx, angle_rx); 336 | self.computed.horizon_distances = (dist_tx, dist_rx); 337 | } 338 | 339 | /// Intermediate values used in some prep calculations. 340 | /// 341 | /// The least of fifteen times the terminal height above ground, and 10% of 342 | /// the horizon distance. 343 | /// 344 | /// Unsure as to what it's about, really, but it's there. 345 | /// 346 | /// See ITM sections `<43>` and `<44>`. 347 | fn get_xl(&mut self) -> (f64, f64) { 348 | #[inline] 349 | fn make_xl(h: f64, horiz_dist: f64) -> f64 { 350 | (15.0 * h).min(0.1 * horiz_dist) 351 | } 352 | 353 | ( 354 | make_xl(self.heights.0, self.computed.horizon_distances.0), 355 | self.length - make_xl(self.heights.1, self.computed.horizon_distances.1), 356 | ) 357 | } 358 | 359 | /// Given initial but naive horizon calculation, make some adjustments. 360 | /// 361 | /// This takes a hybrid approach of using the empirical formulae originally 362 | /// designed for the area mode to compute parameters for what is 363 | /// essentially the point-to-point mode used kinda like the area mode. 364 | /// 365 | /// This implementation does away completely with the distinction and 366 | /// eliminates all mode-switching logic. The original routine also had 367 | /// fallbacks for when a climate was not specified: we forbid that instead. 368 | /// 369 | /// See ITM sections `<43>`, `<45>`, `<46>`. 370 | fn adjust_horizons(&mut self, xl: (f64, f64)) { 371 | let mut q; 372 | let z; 373 | 374 | if self.computed.horizon_distances.0 + self.computed.horizon_distances.1 > 1.5 * self.length 375 | { 376 | // Redo light-of-sight horizons <45> if the path is line-of-sight 377 | 378 | let (_, nz) = z1sq1(self.computed.interval, self.elevations, xl); 379 | z = nz; 380 | self.computed.effective_heights = ( 381 | // he = effective_heights 382 | self.heights.0 + fortran_dim(self.elevations[0], z.0), 383 | self.heights.1 + fortran_dim(self.elevations[self.elevations.len()], z.1), 384 | ); 385 | 386 | fn make_dl(h: f64, curv: f64, terrain: f64) -> f64 { 387 | // curv = gme = effective_curvature 388 | (2.0 * h / curv).sqrt() * (-0.07 * (terrain / h.max(5.0)).sqrt()).exp() 389 | } 390 | 391 | self.computed.horizon_distances = ( 392 | // dl = horizon_distances 393 | make_dl( 394 | self.computed.effective_heights.0, 395 | self.computed.effective_curvature, 396 | self.computed.terrain_irregularity, 397 | ), 398 | make_dl( 399 | self.computed.effective_heights.1, 400 | self.computed.effective_curvature, 401 | self.computed.terrain_irregularity, 402 | ), 403 | ); 404 | 405 | q = self.computed.horizon_distances.0 + self.computed.horizon_distances.1; 406 | 407 | if q <= self.length { 408 | /* if there is a rounded horizon, or two obstructions, in the path */ 409 | q = (self.length / q).powi(2); 410 | 411 | fn make_hedl(q: f64, he: f64, curv: f64, terrain: f64) -> (f64, f64) { 412 | let he = he * q; /* tx effective height set to be path dist/self.computed.interval between obstacles */ 413 | ( 414 | he, 415 | (2.0 * he / curv).sqrt() * (-0.07 * (terrain / he.max(5.0)).sqrt()).exp(), 416 | ) 417 | } 418 | 419 | let hedl = ( 420 | make_hedl( 421 | q, 422 | self.computed.effective_heights.0, 423 | self.computed.effective_curvature, 424 | self.computed.terrain_irregularity, 425 | ), 426 | make_hedl( 427 | q, 428 | self.computed.effective_heights.1, 429 | self.computed.effective_curvature, 430 | self.computed.terrain_irregularity, 431 | ), 432 | ); 433 | 434 | self.computed.effective_heights = ((hedl.0).0, (hedl.1).0); // he 435 | self.computed.horizon_distances = ((hedl.0).1, (hedl.1).1); // dl 436 | } 437 | 438 | /* original empirical adjustment? uses delta-h to adjust grazing angles */ 439 | fn make_qthe(he: f64, curv: f64, terrain: f64, horiz_dist: f64) -> f64 { 440 | let q = (2.0 * he / curv).sqrt(); 441 | (0.65 * terrain * (q / horiz_dist - 1.0) - 2.0 * he) / q 442 | } 443 | 444 | self.computed.elevation_angles = ( 445 | // the / theta 446 | make_qthe( 447 | self.computed.effective_heights.0, 448 | self.computed.effective_curvature, 449 | self.computed.terrain_irregularity, 450 | self.computed.horizon_distances.0, 451 | ), 452 | make_qthe( 453 | self.computed.effective_heights.1, 454 | self.computed.effective_curvature, 455 | self.computed.terrain_irregularity, 456 | self.computed.horizon_distances.1, 457 | ), 458 | ); 459 | } else { 460 | // Get transhorizon effective heights <46> 461 | 462 | let (_, (z0, _)) = z1sq1( 463 | self.computed.interval, 464 | self.elevations, 465 | (xl.0, 0.9 * self.computed.horizon_distances.0), 466 | ); 467 | let (_, (_, z1)) = z1sq1( 468 | self.computed.interval, 469 | self.elevations, 470 | (self.length - 0.9 * self.computed.horizon_distances.1, xl.1), 471 | ); 472 | 473 | self.computed.effective_heights = ( 474 | self.heights.0 + fortran_dim(self.elevations[0], z0), 475 | self.heights.1 + fortran_dim(self.elevations[self.elevations.len() - 1], z1), 476 | ); 477 | } 478 | } 479 | 480 | pub fn attenuation_at(&self, distance_from_tx: f64) -> Result { 481 | if distance_from_tx < 0.0 { 482 | return Err("distance negative".into()); 483 | } 484 | 485 | if distance_from_tx > self.length { 486 | return Err("distance above bounds".into()); 487 | } 488 | 489 | Err("not implemented".into()) 490 | } 491 | } 492 | 493 | /// Input settings for the model. 494 | /// 495 | /// Refer to [ITU-R P.527] to derive ground permittivity and conductivity for 496 | /// your region/terrain and frequency. Splash has related utilities. 497 | /// Alternatively, you may use [SPLAT's simplified table][dielectrics]. 498 | /// 499 | /// [ITU-R P.527]: https://www.itu.int/dms_pubrec/itu-r/rec/p/R-REC-P.527-4-201706-I!!PDF-E.pdf 500 | /// [dielectrics]: http://www.qsl.net/n9zia/conduct.html 501 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 502 | pub struct Settings { 503 | /// Relative permittivity of the ground (aka "dielectric constant"). 504 | pub permittivity: f64, 505 | 506 | /// Relative conductivity of the ground (in siemens per metre). 507 | pub conductivity: f64, 508 | 509 | /// Type of climate. 510 | pub climate: Climate, 511 | 512 | /// Surface refractivity reduced to sea level. 513 | pub surface_refractivity: f64, 514 | 515 | /// Frequency of modeled wave (MHz). 516 | pub frequency: f64, 517 | 518 | /// Polarisation of modeled wave. 519 | pub polarisation: Polarisation, 520 | 521 | // statistical fractions for the final quantisation 522 | pub conf: f64, 523 | pub rel: f64, 524 | } 525 | 526 | /// The inverse of the standard normal complementary probability function. 527 | /// 528 | /// The standard normal complementary function is _Q(x) = 1 / √͞2͞π ∫ e^(-t²/2)_. 529 | /// This inverse is the solution for _x_ to _q = Q(x)_, also noted _Q¯¹(q)_. 530 | /// 531 | /// This function is used to scale the inputs (the desired fractions of time, 532 | /// locations, situations to model) to later obtain normal quantiles. 533 | /// 534 | /// The implementation is not the normal tables, but rather an approximation by 535 | /// [Cecil Hastings][Hastings55], with a maximum error of 4.5 × 10¯⁴. 536 | /// 537 | /// In the FORTRAN, this function was called `qerfi` 538 | /// ("Q error function, inverted"), hence the constants. See <50>, <51>. 539 | /// 540 | /// [Hastings55]: https://press.princeton.edu/titles/1133.html 541 | fn inverse_normal_complementary(q: f64) -> f64 { 542 | let x = 0.5 - q; 543 | let mut t = (0.5 - x.abs()).max(0.000001); 544 | t = (-2.0 * t.ln()).sqrt(); 545 | let v = t 546 | - ((QERFI_C.2 * t + QERFI_C.1) * t + QERFI_C.0) 547 | / (((QERFI_D.2 * t + QERFI_D.1) * t + QERFI_D.0) * t + 1.0); 548 | 549 | if x < 0.0 { 550 | -v 551 | } else { 552 | v 553 | } 554 | } 555 | 556 | /// C group of constants for the qerf/qerfi approximations. 557 | const QERFI_C: (f64, f64, f64) = (2.515516698, 0.802853, 0.010328); 558 | 559 | /// D group of constants for the qerf/qerfi approximations. 560 | const QERFI_D: (f64, f64, f64) = (1.432788, 0.189269, 0.001308); 561 | 562 | /// Dimensionless constant used in the diffraction attenuation function. 563 | /// 564 | /// See T.A. 4.18, 4.19, 4.24, 4.25. 565 | /// 566 | /// In the model <11> the constant is 151.0 instead, but that might just be an approximation for 567 | /// the implementation. Let's use the precise constant as we can. 568 | const DIFFRACTION_CONSTANT_A: f64 = 151.03; 569 | 570 | /// Approximation for the function B(K) in Vogler's formulation of the solution to the smooth, 571 | /// spherical earth problem. 572 | /// 573 | /// This is the original approximation used in the algorithm, referenced 4.15-4.25 and defined in 574 | /// T.A. 6.2. It is not used in Splash's implementation and is only provided as historical detail. 575 | /// Instead, see the [`vogler_b`] function which implements it exactly. 576 | fn vogler_b_approx(k: f64) -> f64 { 577 | 1.607 - k.abs() 578 | } 579 | 580 | /// Fresnel integral approximation from v² 581 | /// 582 | /// This is the approximation used in the provided code, which is the same approximation used in 583 | /// the algorithm, but modified to take v² instead of v. It is not used in Splash's implementation 584 | /// and is only provided as historical detail. Also see the [`fresnel_approx`] function which is 585 | /// the algorithm's approximation, and [`fresnel_integral`] which implements it exactly. 586 | fn fresnel_approx_from_squared(v2: f64) -> f64 { 587 | if v2 < 5.76 { 588 | 6.02 + 9.11 * v2.sqrt() - 1.27 * v2 589 | } else { 590 | 12.953 + 10.0 * v2.log10() 591 | } 592 | } 593 | 594 | /// Fresnel integral approximation from v 595 | /// 596 | /// This is the approximation used in the algorithm, see T.A. 6.1, It is not used in Splash's 597 | /// implementation and is only provided as historical detail. Also see the 598 | /// [`fresnel_approx_from_squared`] function which is the algorithm's approximation, and 599 | /// [`fresnel_integral`] which implements it exactly. 600 | fn fresnel_approx(v: f64) -> f64 { 601 | // Not present in the v² approx, but present in the prose in T.A. 6.1 602 | assert!(v > 0.0, "fresnel approximation does not hold for v <= 0"); 603 | 604 | if v <= 2.40 { 605 | 6.02 + 9.11 * v - 1.27 * v.powi(2) 606 | } else { 607 | 12.953 + 20.0 * v.log10() 608 | } 609 | } 610 | 611 | /// A Fresnel integral used for the model 612 | /// 613 | /// 614 | fn fresnel_integral(v: f64) -> f64 { 615 | todo!("fresnel") 616 | } 617 | 618 | 619 | // below this point is "original" code that, once the legibility and rustification 620 | // process is done, should completely disappear / not be used at all. 621 | 622 | /// Point-to-Point propagation 623 | pub fn point_to_point( 624 | distance: f64, // actually the interval 625 | elevations: &Vec, 626 | tx_height: f64, 627 | rx_height: f64, 628 | permittivity: f64, 629 | conductivity: f64, 630 | surfref: f64, 631 | freq: f64, 632 | climate: Climate, 633 | polarisation: Polarisation, 634 | conf: f64, 635 | rel: f64, 636 | ) -> Result { 637 | let mut propa = PropA::default(); 638 | 639 | let mut prop = Prop::default(); 640 | prop.hg = (tx_height, rx_height); 641 | prop.ens = surfref; 642 | prop.wn = freq / 47.7; 643 | 644 | let mut propv = PropV::default(); 645 | propv.klim = climate; 646 | propv.lvar = 5; 647 | propv.mdvar = 12; 648 | 649 | let subset = &elevations[1..(elevations.len() - 1)]; 650 | let sum: f64 = subset.iter().sum(); 651 | let zsys = sum / (subset.len() as f64); 652 | 653 | qlrps(zsys, polarisation, permittivity, conductivity, &mut prop); 654 | qlrpfl( 655 | distance, 656 | elevations, 657 | propv.mdvar, 658 | &mut prop, 659 | &mut propa, 660 | &mut propv, 661 | ); 662 | 663 | let zc = inverse_normal_complementary(conf); 664 | let zr = inverse_normal_complementary(rel); 665 | let fs = 32.45 + 20.0 * (freq.log10() + (prop.dist / 1000.0).log10()); 666 | let ret = avar(zr, 0.0, zc, &mut prop, &mut propv) + fs; 667 | 668 | if prop.kwx > 0 { 669 | Err(()) 670 | } else { 671 | Ok(ret) 672 | } 673 | } 674 | 675 | // done (prepare_environment) 676 | fn qlrps(zsys: f64, pol: Polarisation, dielect: f64, conduct: f64, prop: &mut Prop) { 677 | let gma = 157e-9; 678 | 679 | if zsys != 0.0 { 680 | prop.ens *= (-zsys / 9460.0).exp(); 681 | } 682 | 683 | prop.gme = gma * (1.0 - 0.04665 * (prop.ens / 179.3).exp()); 684 | 685 | let zq = Complex64::new(dielect, 376.62 * conduct / prop.wn); 686 | let zgnd = (zq - 1.0).sqrt(); 687 | 688 | prop.zgnd = match pol { 689 | Polarisation::Horizontal => zgnd, 690 | Polarisation::Vertical => zgnd / zq, 691 | }; 692 | } 693 | 694 | // mostly done (find_horizons, adjust_horizons) 695 | fn qlrpfl( 696 | distance: f64, 697 | elevations: &Vec, 698 | mdvarx: isize, 699 | mut prop: &mut Prop, 700 | propa: &mut PropA, 701 | propv: &mut PropV, 702 | ) { 703 | prop.dist = elevations.len() as f64 * distance; 704 | let np = elevations.len(); 705 | hzns(distance, &elevations, &mut prop); 706 | 707 | fn make_xl(hg: f64, dl: f64) -> f64 { 708 | (15.0 * hg).min(0.1 * dl) 709 | } 710 | 711 | let mut q; 712 | let z; 713 | let mut xl = (make_xl(prop.hg.0, prop.dl.0), make_xl(prop.hg.1, prop.dl.1)); 714 | 715 | xl.1 = prop.dist - xl.1; 716 | prop.dh = d1thx(distance, elevations, xl); 717 | 718 | if prop.dl.0 + prop.dl.1 > 1.5 * prop.dist { 719 | let (_, nz) = z1sq1(distance, elevations, xl); 720 | z = nz; 721 | prop.he = ( 722 | prop.hg.0 + fortran_dim(elevations[0], z.0), 723 | prop.hg.1 + fortran_dim(elevations[np], z.1), 724 | ); 725 | 726 | fn make_dl(he: f64, gme: f64, dh: f64) -> f64 { 727 | (2.0 * he / gme).sqrt() * (-0.07 * (dh / he.max(5.0)).sqrt()).exp() 728 | } 729 | 730 | prop.dl = ( 731 | make_dl(prop.he.0, prop.gme, prop.dh), 732 | make_dl(prop.he.1, prop.gme, prop.dh), 733 | ); 734 | 735 | q = prop.dl.0 + prop.dl.1; 736 | 737 | if q <= prop.dist { 738 | /* if there is a rounded horizon, or two obstructions, in the path */ 739 | let temp = prop.dist / q; 740 | q = temp * temp; 741 | 742 | fn make_hedl(q: f64, he: f64, gme: f64, dh: f64) -> (f64, f64) { 743 | let he = he * q; /* tx effective height set to be path dist/distance between obstacles */ 744 | ( 745 | he, 746 | (2.0 * he / gme).sqrt() * (-0.07 * (dh / he.max(5.0)).sqrt()).exp(), 747 | ) 748 | } 749 | 750 | let hedl = ( 751 | make_hedl(q, prop.he.0, prop.gme, prop.dh), 752 | make_hedl(q, prop.he.1, prop.gme, prop.dh), 753 | ); 754 | 755 | prop.he = ((hedl.0).0, (hedl.1).0); 756 | prop.dl = ((hedl.0).1, (hedl.1).1); 757 | } 758 | 759 | /* original empirical adjustment? uses delta-h to adjust grazing angles */ 760 | fn make_qthe(he: f64, gme: f64, dh: f64, dl: f64) -> f64 { 761 | let q = (2.0 * he / gme).sqrt(); 762 | (0.65 * dh * (q / dl - 1.0) - 2.0 * he) / q 763 | } 764 | 765 | prop.the = ( 766 | make_qthe(prop.he.0, prop.gme, prop.dh, prop.dl.0), 767 | make_qthe(prop.he.1, prop.gme, prop.dh, prop.dl.1), 768 | ); 769 | } else { 770 | let (_, (z0, _)) = z1sq1(distance, elevations, (xl.0, 0.9 * prop.dl.0)); 771 | let (_, (_, z1)) = z1sq1(distance, elevations, (prop.dist - 0.9 * prop.dl.1, xl.1)); 772 | 773 | prop.he = ( 774 | prop.hg.0 + fortran_dim(elevations[0], z0), 775 | prop.hg.1 + fortran_dim(elevations[np - 1], z1), 776 | ); 777 | } 778 | 779 | propv.lvar = propv.lvar.max(3); 780 | 781 | if mdvarx >= 0 { 782 | propv.mdvar = mdvarx; 783 | propv.lvar = propv.lvar.max(4); 784 | } 785 | 786 | propv.lvar = 5; 787 | 788 | // the big compute 789 | lrprop(0.0, prop, propa); 790 | } 791 | 792 | // done (find_horizons) 793 | fn hzns(distance: f64, elevations: &Vec, prop: &mut Prop) { 794 | let np = elevations.len(); 795 | let xi = distance; 796 | 797 | let za = elevations[0] + prop.hg.0; 798 | let zb = elevations[np - 1] + prop.hg.1; 799 | 800 | let qc = 0.5 * prop.gme; 801 | let mut q = qc * prop.dist; 802 | 803 | prop.the.1 = (zb - za) / prop.dist; 804 | prop.the.0 = prop.the.1 - q; 805 | prop.the.1 = -prop.the.1 - q; 806 | 807 | prop.dl = (prop.dist, prop.dist); 808 | 809 | if np >= 2 { 810 | let mut sa = 0.0; 811 | let mut sb = prop.dist; 812 | let mut wq = true; 813 | 814 | for i in 1..np { 815 | sa += xi; 816 | sb -= xi; 817 | q = elevations[i] - (qc * sa + prop.the.0) * sa - za; 818 | 819 | if q > 0.0 { 820 | prop.the.0 += q / sa; 821 | prop.dl.0 = sa; 822 | wq = false; 823 | } 824 | 825 | if !wq { 826 | q = elevations[i] - (qc * sb + prop.the.1) * sb - zb; 827 | 828 | if q > 0.0 { 829 | prop.the.1 += q / sb; 830 | prop.dl.1 = sb; 831 | } 832 | } 833 | } 834 | } 835 | } 836 | 837 | // <48> ("delta h over x", the interdecile range of elevations between x1 & x2) 838 | fn d1thx(distance: f64, elevations: &Vec, xl: (f64, f64)) -> f64 { 839 | let np = elevations.len(); 840 | let mut xa = xl.0 / distance; 841 | let xb = xl.1 / distance; 842 | 843 | if (xb - xa) < 2.0 { 844 | return 0.0; 845 | } 846 | 847 | let mut ka = (0.1 * (xb - xa + 8.0)) as usize; 848 | ka = 4.max(ka).min(25); 849 | 850 | let n = 10 * ka - 5; 851 | let kb = n - ka + 1; 852 | let sn = n - 1; 853 | let mut s = Vec::with_capacity(n); 854 | 855 | let xb = (xb - xa) / (sn as f64); 856 | let mut k = (xa + 1.0) as usize; 857 | xa -= k as f64; 858 | 859 | for j in 0..n { 860 | while xa > 0.0 && k < np { 861 | xa -= 1.0; 862 | k += 1; 863 | } 864 | 865 | s[j] = elevations[k] + (elevations[k] - elevations[k - 1]) * xa; 866 | xa = xa + xb; 867 | } 868 | 869 | let ((_, sn), (mut xa, mut xb)) = z1sq1(1.0, &s, (0.0, sn as f64)); 870 | xb = (xb - xa) / (sn as f64); 871 | 872 | for j in 0..n { 873 | s[j] -= xa; 874 | xa = xa + xb; 875 | } 876 | 877 | let d1thxv = qtile(n - 1, &mut s, ka - 1) - qtile(n - 1, &mut s, kb - 1); 878 | d1thxv / (1.0 - 0.8 * (-(xl.1 - xl.0) / 50.0e3).exp()) 879 | } 880 | 881 | // transliteration typo of zlsql 882 | fn z1sq1(interval: f64, elevations: &Vec, x: (f64, f64)) -> ((f64, f64), (f64, f64)) { 883 | zlsql(interval, elevations, x) 884 | } 885 | 886 | // <53> done in least_squares_linear_fit 887 | fn zlsql(interval: f64, elevations: &Vec, x: (f64, f64)) -> ((f64, f64), (f64, f64)) { 888 | (x, least_squares_linear_fit(interval, elevations, x)) 889 | } 890 | 891 | // <52>. provides a quantile 892 | fn qtile(nn: usize, elevations: &mut Vec, ir: usize) -> f64 { 893 | let mut m = 0; 894 | let mut n = nn; 895 | let k = 0.max(ir).min(n); 896 | 897 | let mut q = 0.0; 898 | let mut r: f64; 899 | let mut j: usize; 900 | let mut j1 = 0; 901 | let mut i0 = 0; 902 | 903 | let mut i; 904 | let mut goto10 = true; 905 | loop { 906 | if goto10 { 907 | q = elevations[k]; 908 | i0 = m; 909 | j1 = n; 910 | } 911 | 912 | i = i0; 913 | 914 | while i <= n && elevations[i] >= q { 915 | i += 1; 916 | } 917 | 918 | if i > n { 919 | i = n; 920 | } 921 | 922 | j = j1; 923 | 924 | while j >= m && elevations[j] <= q { 925 | j -= 1; 926 | } 927 | 928 | if j < m { 929 | j = m; 930 | } 931 | 932 | if i < j { 933 | r = elevations[i]; 934 | elevations[i] = elevations[j]; 935 | elevations[j] = r; 936 | i0 = i + 1; 937 | j1 = j - 1; 938 | goto10 = false; 939 | } else if i < k { 940 | elevations[k] = elevations[i]; 941 | elevations[i] = q; 942 | m = i + 1; 943 | goto10 = true; 944 | } else if j > k { 945 | elevations[k] = elevations[j]; 946 | elevations[j] = q; 947 | n = j - 1; 948 | goto10 = true; 949 | } else { 950 | break; 951 | } 952 | } 953 | 954 | return q; 955 | } 956 | 957 | // <4>, <5>, the actual propagation compute 958 | fn lrprop(d: f64, prop: &mut Prop, propa: &mut PropA) { 959 | // first part is mostly input checks 960 | // "kwx" is the error output... higher is worse, but really anything 961 | // besides zero is bad, so during rewrite just abort early anytime. 962 | 963 | if !prop.setup_done { 964 | // --- <6> --- prep secondary params 965 | 966 | propa.dls.0 = (2.0 * prop.he.0 / prop.gme).sqrt(); 967 | propa.dls.1 = (2.0 * prop.he.1 / prop.gme).sqrt(); 968 | 969 | propa.dlsa = propa.dls.0 + propa.dls.1; 970 | propa.dla = prop.dl.0 + prop.dl.1; 971 | propa.tha = (prop.the.0 + prop.the.1).max(-propa.dla * prop.gme); 972 | 973 | // --- <7> --- checks ranges 974 | 975 | if prop.wn < 0.838 || prop.wn > 210.0 { 976 | prop.kwx = prop.kwx.max(1); 977 | } 978 | 979 | fn make_kwx_hg(kwx: usize, hg: f64) -> usize { 980 | if hg < 1.0 || hg > 1000.0 { 981 | kwx.max(1) 982 | } else { 983 | kwx 984 | } 985 | } 986 | 987 | prop.kwx = make_kwx_hg(prop.kwx, prop.hg.0); 988 | prop.kwx = make_kwx_hg(prop.kwx, prop.hg.1); 989 | 990 | fn make_kwx_dl(kwx: usize, the: f64, dl: f64, dls: f64) -> usize { 991 | if (the.abs() > 200e-3) || (dl < 0.1 * dls) || (dl > 3.0 * dls) { 992 | kwx.max(3) 993 | } else { 994 | kwx 995 | } 996 | } 997 | 998 | prop.kwx = make_kwx_dl(prop.kwx, prop.the.0, prop.dl.0, propa.dls.0); 999 | prop.kwx = make_kwx_dl(prop.kwx, prop.the.1, prop.dl.1, propa.dls.1); 1000 | 1001 | if (prop.ens < 250.0) 1002 | || (prop.ens > 400.0) 1003 | || (prop.gme < 75e-9) 1004 | || (prop.gme > 250e-9) 1005 | || (prop.zgnd.re <= prop.zgnd.im.abs()) 1006 | || (prop.wn < 0.419) 1007 | || (prop.wn > 420.0) 1008 | { 1009 | prop.kwx = 4; // fail here 1010 | } 1011 | 1012 | fn make_kwx_hg_again(kwx: usize, hg: f64) -> usize { 1013 | if hg < 0.5 || hg > 3000.0 { 1014 | 4 // fail here 1015 | } else { 1016 | kwx 1017 | } 1018 | } 1019 | 1020 | prop.kwx = make_kwx_hg_again(prop.kwx, prop.hg.0); 1021 | prop.kwx = make_kwx_hg_again(prop.kwx, prop.hg.1); 1022 | 1023 | // --- <9> --- diffraction coefficients --- see T.A. 4.2 through 4.8 1024 | prop.dmin = (prop.he.0 - prop.he.1).abs() / 200e-3; 1025 | let q = adiff(0.0, prop, propa); 1026 | prop.xae = (prop.wn * prop.gme.powi(2)).cbrt(); 1027 | let d3 = propa.dlsa.max(1.3787 * prop.xae + propa.dla); 1028 | let d4 = d3 + 2.7574 * prop.xae; 1029 | let a3 = adiff(d3, prop, propa); 1030 | let a4 = adiff(d4, prop, propa); 1031 | propa.emd = (a4 - a3) / (d4 - d3); 1032 | propa.aed = a3 - propa.emd * d3; 1033 | } 1034 | 1035 | prop.dist = d; 1036 | 1037 | // <8> distance bounds checks 1038 | if prop.dist > 0.0 { 1039 | if prop.dist > 1000e3 { 1040 | prop.kwx = prop.kwx.max(1); 1041 | } 1042 | 1043 | if prop.dist < prop.dmin { 1044 | prop.kwx = prop.kwx.max(3); 1045 | } 1046 | 1047 | if prop.dist < 1e3 || prop.dist > 2000e3 { 1048 | prop.kwx = 4; // fail here 1049 | } 1050 | } 1051 | 1052 | // <15> line of sight calculations 1053 | if prop.dist < propa.dlsa { 1054 | if !prop.wlos { 1055 | // <16> prep constants on first run 1056 | alos(0.0, prop, propa); 1057 | let d2 = propa.dlsa; 1058 | let a2 = propa.aed + d2 * propa.emd; 1059 | let mut d0 = 1.908 * prop.wn * prop.he.0 * prop.he.1; // T.A. 4.38 1060 | 1061 | let d1; 1062 | if propa.aed >= 0.0 { 1063 | d0 = d0.min(0.5 * propa.dla); // T.A. 4.28 1064 | d1 = d0 + 0.25 * (propa.dla - d0); // T.A. 4.29 1065 | } else { 1066 | d1 = (-propa.aed / propa.emd).max(0.25 * propa.dla); // T.A. 4.30 1067 | } 1068 | 1069 | let a1 = alos(d1, prop, propa); // T.A. 4.31 1070 | 1071 | if d0 < d1 { 1072 | let a0 = alos(d0, prop, propa); // T.A. 4.30 1073 | let q = (d2 / d0).ln(); 1074 | propa.ak2 = 0.0f64.max( 1075 | ((d2 - d0) * (a1 - a0) - (d1 - d0) * (a2 - a0)) 1076 | / ((d2 - d0) * (d1 / d0).ln() - (d1 - d0) * q), 1077 | ); // T.A. 4.32 1078 | 1079 | let wq = propa.aed >= 0.0 || propa.ak2 > 0.0; 1080 | 1081 | if wq { 1082 | propa.ak1 = (a2 - a0 - propa.ak2 * q) / (d2 - d0); // T.A. 4.33 1083 | 1084 | if propa.ak1 < 0.0 { 1085 | propa.ak1 = 0.0; // T.A. 4.36 1086 | propa.ak2 = fortran_dim(a2, a0) / q; // T.A. 4.35 1087 | 1088 | // T.A. 4.37 1089 | if propa.ak2 == 0.0 { 1090 | propa.ak1 = propa.emd; 1091 | } 1092 | } 1093 | } else { 1094 | propa.ak1 = (a2 - a1) / (d2 - d1); // T.A. 4.40 1095 | propa.ak2 = 0.0; // T.A. 4.41 1096 | 1097 | // T.A. 4.37 1098 | if propa.ak1 <= 0.0 { 1099 | propa.ak1 = propa.emd; 1100 | } 1101 | } 1102 | } else { 1103 | // same as above 1104 | propa.ak1 = (a2 - a1) / (d2 - d1); 1105 | propa.ak2 = 0.0; 1106 | 1107 | if propa.ak1 <= 0.0 { 1108 | propa.ak1 = propa.emd; 1109 | } 1110 | } 1111 | 1112 | // T.A. 4.42 1113 | propa.ael = a2 - propa.ak1 * d2 - propa.ak2 * d2.ln(); 1114 | 1115 | prop.wlos = true; 1116 | } 1117 | 1118 | // Do calculation when given real distance (dist = 0 is constant prep) 1119 | if prop.dist > 0.0 { 1120 | // T.A. 4.1 1121 | prop.aref = propa.ael + propa.ak1 * prop.dist + propa.ak2 * prop.dist.ln(); 1122 | } 1123 | } 1124 | 1125 | // <20> troposcatter calculations 1126 | if prop.dist <= 0.0 || prop.dist >= propa.dlsa { 1127 | if !prop.wscat { 1128 | // <21> -- setup constants 1129 | ascat(0.0, prop, propa); 1130 | let d5 = propa.dla + 200e3; // T.A. 4.52 1131 | let d6 = d5 + 200e3; // T.A. 4.53 1132 | let a6 = ascat(d6, prop, propa); // T.A. 4.54 1133 | let a5 = ascat(d5, prop, propa); // T.A. 4.55 1134 | 1135 | if a5 < 1000.0 { 1136 | propa.ems = (a6 - a5) / 200e3; // T.A. 4.57 1137 | propa.dx = propa.dlsa.max( 1138 | (propa.dla + 0.3 * prop.xae * (47.7 * prop.wn).ln()) 1139 | .max((a5 - propa.aed - propa.ems * d5) / (propa.emd - propa.ems)), 1140 | ); // T.A. 4.58 1141 | propa.aes = (propa.emd - propa.ems) * propa.dx + propa.aed; // T.A. 4.59 1142 | } else { 1143 | propa.ems = propa.emd; 1144 | propa.aes = propa.aed; 1145 | propa.dx = 10.0e6; // T.A. 4.56 1146 | } 1147 | 1148 | prop.wscat = true; 1149 | } 1150 | 1151 | // T.A. 4.1 1152 | if prop.dist > propa.dx { 1153 | prop.aref = propa.aes + propa.ems * prop.dist; 1154 | } else { 1155 | prop.aref = propa.aed + propa.emd * prop.dist; 1156 | } 1157 | } 1158 | 1159 | prop.aref = prop.aref.max(0.0); 1160 | } 1161 | 1162 | // <10> diffraction attenuation at distance d from site 1163 | fn adiff(d: f64, prop: &mut Prop, propa: &mut PropA) -> f64 { 1164 | // first call with d == 0.0 is used to setup constants 1165 | // should be extracted into two functions and the constants stored in a 1166 | // struct or the cached structure or something. 1167 | if d == 0.0 { 1168 | // see <11> 1169 | let mut q = prop.hg.1 * prop.hg.1; 1170 | let qk = prop.he.0 * prop.he.1 - q; 1171 | 1172 | // if prop.mode == Mode::PointToPoint { 1173 | // q += 10.0; 1174 | // } 1175 | 1176 | // "parts of Q" see T.A. 4.9 1177 | let wd1 = (1.0 + qk / q).sqrt(); 1178 | let xd1 = propa.dla + propa.tha / prop.gme; 1179 | 1180 | // Afo is a ‘clutter’ function (empirical): approximately accounts for the median 1181 | // additional diffraction attenuation due to additional knife-edges between the terminals’ 1182 | // irregular terrain radio horizons that may obstruct the convex hull between the two 1183 | // irregular terrain radio horizons. 1184 | // T.A. 4.10 1185 | q = (1.0 - 0.8 * (-propa.dlsa / 50e3).exp()) * prop.dh; 1186 | q *= 0.78 * (-(q / 16.0).powf(0.25)).exp(); 1187 | let afo = 15.0f64.min(2.171 * (1.0 + 4.77e-4 * prop.hg.0 * prop.hg.1 * prop.wn * q).ln()); 1188 | 1189 | // T.A. 6.7 1190 | let qk = 1.0 / prop.zgnd.norm_sqr().sqrt(); 1191 | let mut aht = 20.0; 1192 | let mut xht = 0.0; 1193 | 1194 | fn make_axht(dl: f64, he: f64, wn: f64, qk: f64) -> (f64, f64) { 1195 | let a = 0.5 * dl.powi(2) / he; // T.A. 4.15 1196 | let wa = (a * wn).cbrt(); // T.A. 4.16 1197 | let pk = qk / wa; // T.A. 4.17 1198 | let q = vogler_b_approx(pk) * DIFFRACTION_CONSTANT_A * wa * dl / a; // T.A. 4.18 1199 | (q, fht(q, pk)) 1200 | } 1201 | 1202 | let (x, a) = make_axht(prop.dl.0, prop.he.0, prop.wn, qk); 1203 | xht += x; 1204 | aht += a; 1205 | 1206 | let (x, a) = make_axht(prop.dl.1, prop.he.1, prop.wn, qk); 1207 | xht += x; 1208 | aht += a; 1209 | 1210 | 0.0 // returns 0 just because this is the dummy setup round 1211 | } else { 1212 | // see <12> 1213 | 1214 | // T.A. 4.12 1215 | let th = propa.tha + d * prop.gme; 1216 | let ds = d - propa.dla; 1217 | let q = 0.0795775 * prop.wn * ds * th * th; 1218 | 1219 | // The knife-edge diffraction function 1220 | // T.A. 4.14 1221 | let adiffv = 1222 | aknfe(q * prop.dl.0 / (ds + prop.dl.0)) + aknfe(q * prop.dl.1 / (ds + prop.dl.1)); 1223 | 1224 | // Dummy values to get this compiling -- they're from the constants run 1225 | let qk = 0.0; 1226 | let xht = 0.0; 1227 | let aht = 0.0; 1228 | let wd1 = 0.0; 1229 | let xd1 = 0.0; 1230 | let afo = 0.0; 1231 | 1232 | // T.A. 4.16 1233 | let a = ds / th; 1234 | let wa = (a * prop.wn).cbrt(); 1235 | let pk = qk / wa; // T.A. 4.17 1236 | let mut q = vogler_b_approx(pk) * DIFFRACTION_CONSTANT_A * wa * th + xht; // T.A. 4.18 and 6.2 1237 | let ar = 0.05751 * q - 4.343 * q.ln() - aht; // T.A. 4.20 1238 | q = (wd1 + xd1 / d) * 6283.2f64.min((1.0 - 0.8 * (-d / 50e3).exp()) * prop.dh * prop.wn); 1239 | 1240 | // T.A. 4.9 1241 | let wd = 25.1 / (25.1 + q.sqrt()); 1242 | 1243 | // T.A. 4.11 1244 | ar * wd + (1.0 - wd) * adiffv + afo 1245 | } 1246 | } 1247 | 1248 | // <17> line of sight attenuation at distance d from site 1249 | fn alos(d: f64, prop: &mut Prop, propa: &mut PropA) -> f64 { 1250 | let mut wls = 0.0; 1251 | 1252 | // see adiff comment on constant gen and splitting 1253 | if d == 0.0 { 1254 | // <18> 1255 | // T.A. 4.43 1256 | wls = 0.021 / (0.021 + prop.wn * prop.dh / 10e3f64.max(propa.dlsa)); 1257 | 0.0 1258 | } else { 1259 | // <19> 1260 | let mut q = (1.0 - 0.8 * (-d / 50e3).exp()) * prop.dh; 1261 | let s = 0.78 * q * (-(q / 16.0).powf(0.25)).exp(); 1262 | q = prop.he.0 + prop.he.1; 1263 | let sps = q / (d.powi(2) + q.powi(2)).sqrt(); 1264 | 1265 | // T.A. 4.47 1266 | let mut r = (sps - prop.zgnd) / (sps + prop.zgnd) * (-10.0f64.min(prop.wn * s * sps)).exp(); 1267 | q = r.norm_sqr(); 1268 | 1269 | // T.A. 4.48 1270 | if q < 0.25 || q < sps { 1271 | r = r * (sps / q).sqrt(); 1272 | } 1273 | 1274 | let alosv = propa.emd * d + propa.aed; // T.A. 4.45 1275 | q = prop.wn * prop.he.0 * prop.he.1 * 2.0 / d; // T.A. 4.49 1276 | 1277 | // T.A. 4.50 1278 | if q > 1.57 { 1279 | q = 3.14 - 2.4649 / q; 1280 | } 1281 | 1282 | // T.A. 4.51 and 4.44 1283 | let qq = Complex64::new(q.cos(), -q.sin()); 1284 | (-4.343 * (qq + r).norm_sqr().ln() - alosv) * wls + alosv 1285 | } 1286 | } 1287 | 1288 | // <22> scatter attenuation at distance d from site 1289 | // See TN101 for approximation method description 1290 | fn ascat(d: f64, prop: &mut Prop, propa: &mut PropA) -> f64 { 1291 | // static double ad, rr, etq, h0s; 1292 | // double h0, r1, r2, z0, ss, et, ett, th, q; 1293 | // double ascatv, temp; 1294 | 1295 | // see adiff comment on constant gen and splitting 1296 | if d == 0.0 { 1297 | // <23> 1298 | prop.ad = prop.dl.0 - prop.dl.1; 1299 | prop.rr = prop.he.1 / prop.rch.0; 1300 | 1301 | if prop.ad < 0.0 { 1302 | prop.ad = -prop.ad; 1303 | prop.rr = 1.0 / prop.rr; 1304 | } 1305 | 1306 | // T.A. 4.67 (partial) 1307 | prop.etq = (5.67e-6 * prop.ens - 2.32e-3) * prop.ens + 0.031; 1308 | prop.h0s = -15.0; 1309 | 0.0 1310 | } else { 1311 | // <24> 1312 | let mut h0; 1313 | if prop.h0s > 15.0 { 1314 | h0 = prop.h0s; 1315 | } else { 1316 | let th = prop.the.0 + prop.the.1 + d * prop.gme; // T.A. 4.61 1317 | 1318 | // T.A. 4.62 1319 | let mut r2 = 2.0 * prop.wn * th; 1320 | let r1 = r2 * prop.he.0; 1321 | r2 *= prop.he.1; 1322 | 1323 | // bounds check. 1001 is "error" value to exit out 1324 | if r1 < 0.2 && r2 < 0.2 { 1325 | return 1001.0; 1326 | } 1327 | 1328 | let mut ss = (d - prop.ad) / (d + prop.ad); // T.A. 4.65 1329 | 1330 | // T.A. 4.66 1331 | let mut q = prop.rr / ss; 1332 | ss = ss.max(0.1); 1333 | q = q.max(0.1).min(10.0); 1334 | let z0 = (d - prop.ad) * (d + prop.ad) * th * 0.25 / d; 1335 | 1336 | // T.A. 4.67 1337 | let temp = (z0 / 8.0e3).min(1.7).powi(6); 1338 | let et = (prop.etq * (-temp).exp() + 1.0) * z0 / 1.7556e3; 1339 | let ett = et.max(1.0); 1340 | 1341 | h0 = (h0f(r1, ett) + h0f(r2, ett)) * 0.5; // T.A. 6.12 1342 | h0 += 1.38 - ett.ln().min(h0) * ss.ln() * q.ln() * 0.49; // T.A. 6.10 and 6.11 1343 | h0 = fortran_dim(h0, 0.0); 1344 | 1345 | // T.A. 6.14 1346 | if et < 1.0 { 1347 | let temp = (1.0 + 1.4142 / r1) * (1.0 + 1.4142 / r2); 1348 | h0 = et * h0 1349 | + (1.0 - et) * 4.343 * (temp.powi(2) * (r1 + r2) / (r1 + r2 + 2.8284)).ln(); 1350 | } 1351 | 1352 | // calc got out of bounds, revert back 1353 | if h0 > 15.0 && prop.h0s >= 0.0 { 1354 | h0 = prop.h0s; 1355 | } 1356 | } 1357 | 1358 | prop.h0s = h0; 1359 | 1360 | // T.A. 4.60 1361 | let th = propa.tha + d * prop.gme; 1362 | 1363 | // T.A. 4.63 and 6.8 1364 | ahd(th * d) + 4.343 * (47.7 * prop.wn * th.powi(4)).ln() 1365 | - 0.1 * (prop.ens - 301.0) * (-th * d / 40e3).exp() 1366 | + h0 1367 | } 1368 | } 1369 | 1370 | // <13> attenuation on a single knife edge 1371 | // this is an approximation of a Fresnel integral, see T.A. 6.1 1372 | // and T.A. 4.21 for the exact form 1373 | fn aknfe(v2: f64) -> f64 { 1374 | fresnel_approx(v2.sqrt()) 1375 | } 1376 | 1377 | // <14> Height gain over geodesic model -- here instead approximated to a 1378 | // smooth spherical earth. See T.A. 6.4 1379 | fn fht(x: f64, pk: f64) -> f64 { 1380 | if x < 200.0 { 1381 | let w = -pk.ln(); 1382 | 1383 | if pk < 1.0e-5 || x * w.powi(3) > 5495.0 { 1384 | if x > 1.0 { 1385 | // this is changed from the original! to be investigated 1386 | // <14> has 40.0 as 17.372 ref T.A. 6.5 1387 | 40.0 * x.log10() - 117.0 1388 | } else { 1389 | -117.0 1390 | } 1391 | } else { 1392 | // T.A. 6.6 1393 | 2.5e-5 * x.powi(2) / pk - 8.686 * w - 15.0 1394 | } 1395 | } else { 1396 | // T.A. 6.3 1397 | let fhtv = 0.05751 * x - 10.0 * x.log10(); 1398 | 1399 | if x < 2000.0 { 1400 | let w = 0.0134 * x * (-0.005 * x).exp(); 1401 | (1.0 - w) * fhtv + w * (40.0 * x.log10() - 117.0) // T.A. 6.4 1402 | } else { 1403 | fhtv 1404 | } 1405 | } 1406 | } 1407 | 1408 | // <25> H01 function for scatter fields, see T.A. §6 1409 | fn h0f(r: f64, et: f64) -> f64 { 1410 | let a = [25.0, 80.0, 177.0, 395.0, 705.0]; 1411 | let b = [24.0, 45.0, 68.0, 80.0, 105.0]; 1412 | 1413 | let (it, q) = if et <= 0.0 { 1414 | (1, 0.0) 1415 | } else if et >= 5.0 { 1416 | (5, 0.0) 1417 | } else { 1418 | (et as usize, et.fract()) 1419 | }; 1420 | 1421 | let x = (1.0 / r).powi(2); 1422 | let h0fv = 4.343 * ((a[it - 1] * x + b[it - 1]) * x + 1.0).ln(); // T.A. 6.13 1423 | 1424 | if q == 0.0 { 1425 | h0fv 1426 | } else { 1427 | (1.0 - q) * h0fv + q * 4.343 * ((a[it] * x + b[it]) * x + 1.0).ln() 1428 | } 1429 | } 1430 | 1431 | // <26> F(theta d) function for scatter fields 1432 | fn ahd(td: f64) -> f64 { 1433 | let a = [133.4, 104.6, 71.8]; 1434 | let b = [0.332e-3, 0.212e-3, 0.157e-3]; 1435 | let c = [-4.343, -1.086, 2.171]; 1436 | 1437 | // choice of constants 1438 | let i = if td <= 10e3 { 1439 | 0 1440 | } else if td <= 70e3 { 1441 | 1 1442 | } else { 1443 | 2 1444 | }; 1445 | 1446 | a[i] + b[i] * td + c[i] * td.ln() // T.A. 6.9 1447 | } 1448 | 1449 | const RT: f64 = 7.8; 1450 | const RL: f64 = 24.0; 1451 | 1452 | // <27> "the statistics" 1453 | #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] 1454 | pub struct PropV { 1455 | // used in avar 1456 | pub sgc: f64, // stddev of confidence -- an output of avar 1457 | 1458 | pub lvar: isize, // control switch 1459 | // this is an optimisation in the original program and will need to be 1460 | // rewritten out, with the different sections it controls split out. 1461 | // the idea is to run some preparation functions only when needed, but 1462 | // it makes the whole thing incomprehensible. See <28> for more. 1463 | pub mdvar: isize, // variability mode switch 1464 | pub klim: Climate, 1465 | } 1466 | 1467 | // <28> 1468 | fn avar(zzt: f64, zzl: f64, zzc: f64, prop: &mut Prop, propv: &mut PropV) -> f64 { 1469 | // static int kdv; 1470 | // static bool ws, w1; 1471 | let mut kdv = 0; 1472 | let mut ws = false; 1473 | let mut w1 = false; 1474 | 1475 | // static double dexa, de, vs0, sgl, sgtm, sgtp, sgtd, tgtd 1476 | let mut dexa = 0.0; 1477 | let mut de = 0.0; 1478 | let mut vmd = 0.0; 1479 | let mut vs0 = 0.0; 1480 | let mut sgl = 0.0; 1481 | let mut sgtm = 0.0; 1482 | let mut sgtp = 0.0; 1483 | let mut sgtd = 0.0; 1484 | let mut tgtd = 0.0; 1485 | // ^ the "constants" to be set up (also see lvar) <27> 1486 | 1487 | // <29>, <30>, <32> select the set of constants to be used for climate adjustments 1488 | let cc: ClimateConstants = propv.klim.into(); 1489 | 1490 | if propv.lvar > 0 { 1491 | // <31> 1492 | match propv.lvar { 1493 | 4 => { 1494 | // <33> 1495 | kdv = propv.mdvar; 1496 | ws = kdv >= 20; 1497 | if ws { 1498 | kdv -= 20; 1499 | } 1500 | 1501 | w1 = kdv >= 10; 1502 | if w1 { 1503 | kdv -= 10; 1504 | } 1505 | 1506 | if kdv < 0 || kdv > 3 { 1507 | kdv = 0; 1508 | prop.kwx = prop.kwx.max(2); 1509 | } 1510 | } 1511 | 2 => { 1512 | // <35> system 1513 | dexa = (18e6 * prop.he.0).sqrt() 1514 | + (18e6 * prop.he.1).sqrt() 1515 | + (575.7e12 / prop.wn).cbrt(); 1516 | } 1517 | 1 => { 1518 | // <36> distance 1519 | de = if prop.dist < dexa { 1520 | 130e3 * prop.dist / dexa 1521 | } else { 1522 | 130e3 + prop.dist - dexa 1523 | }; 1524 | } 1525 | _ => {} 1526 | } 1527 | 1528 | // <32> climate 1529 | let refs = cc.reference_values(de, prop.wn); 1530 | vmd = refs.0; 1531 | sgtm = refs.1; 1532 | sgtp = refs.2; 1533 | sgtd = refs.3; 1534 | tgtd = refs.4; 1535 | 1536 | // <36> distance again 1537 | sgl = if w1 { 1538 | 0.0 1539 | } else { 1540 | let q = (1.0 - 0.8 * (-prop.dist / 50e3).exp()) * prop.dh * prop.wn; 1541 | 10.0 * q / (q + 13.0) 1542 | }; 1543 | 1544 | // <36> still distance 1545 | vs0 = if ws { 1546 | 0.0 1547 | } else { 1548 | (5.0 + 3.0 * (-de / 100e3).exp()).powi(2) 1549 | }; 1550 | 1551 | propv.lvar = 0; 1552 | } 1553 | 1554 | let mut zt = zzt; 1555 | let mut zl = zzl; 1556 | 1557 | // <37> normal deviates 1558 | match kdv { 1559 | 0 => { 1560 | zt = zzc; 1561 | zl = zzc; 1562 | } 1563 | 1 => { 1564 | zl = zzc; 1565 | } 1566 | 2 => { 1567 | zl = zt; 1568 | } 1569 | _ => {} 1570 | }; 1571 | 1572 | // <37> 1573 | if zt.abs() > 3.1 || zl.abs() > 3.1 || zzc.abs() > 3.1 { 1574 | prop.kwx = prop.kwx.max(1); 1575 | } 1576 | 1577 | // <38> resolve standard deviations 1578 | let sgt = if zt < 0.0 { 1579 | sgtm 1580 | } else if zt <= cc.zd { 1581 | sgtp 1582 | } else { 1583 | sgtd + tgtd / zt 1584 | }; 1585 | let vs = vs0 + (sgt * zt).powi(2) / (RT + zzc * zzc) + (sgl * zl).powi(2) / (RL + zzc * zzc); 1586 | 1587 | // <39> resolve deviations yr, yc 1588 | let (yr, sgc) = match kdv { 1589 | 0 => (0.0, sgt.powi(2) + sgl.powi(2) + vs), 1590 | 1 => (sgt * zt, sgl.powi(2) + vs), 1591 | 2 => ((sgt.powi(2) + sgl.powi(2)).sqrt() * zt, vs), 1592 | _ => (sgt * zt + sgl * zl, vs), 1593 | }; 1594 | propv.sgc = sgc.sqrt(); 1595 | 1596 | // T.A. 5.1 1597 | let avarv = prop.aref - vmd - yr - propv.sgc * zzc; 1598 | if avarv < 0.0 { 1599 | avarv * (29.0 - avarv) / (29.0 - 10.0 * avarv) // T.A. 5.2 1600 | } else { 1601 | avarv 1602 | } 1603 | } 1604 | 1605 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 1606 | pub struct Prop { 1607 | /// Reference attenuation 1608 | pub aref: f64, 1609 | 1610 | /// Distance from tx to rx 1611 | pub dist: f64, 1612 | 1613 | /// Antenna structural heights (tx, rx) 1614 | pub hg: (f64, f64), 1615 | 1616 | pub rch: (f64, f64), 1617 | 1618 | /// Wave number (radio frequency) 1619 | pub wn: f64, 1620 | 1621 | /// Terrain irregularity parameter 1622 | pub dh: f64, 1623 | 1624 | pub dhd: f64, 1625 | 1626 | /// Surface refractivity 1627 | pub ens: f64, 1628 | 1629 | pub encc: f64, 1630 | pub cch: f64, 1631 | pub cd: f64, 1632 | 1633 | /// Earth's effective curvature 1634 | pub gme: f64, 1635 | 1636 | /// Surface transfer impedance to the ground 1637 | pub zgnd: Complex64, 1638 | 1639 | /// Antenna effective heights (tx, rx) 1640 | pub he: (f64, f64), 1641 | 1642 | /// Horizon distances (tx, rx) 1643 | pub dl: (f64, f64), 1644 | 1645 | /// Horizon elevation angles (tx, rx) 1646 | pub the: (f64, f64), 1647 | 1648 | pub tiw: f64, 1649 | pub ght: f64, 1650 | pub ghr: f64, 1651 | pub rph: f64, 1652 | pub hht: f64, 1653 | pub hhr: f64, 1654 | pub tgh: f64, 1655 | pub tsgh: f64, 1656 | pub thera: f64, 1657 | pub thenr: f64, 1658 | pub rpl: isize, 1659 | 1660 | /// Error indicator 1661 | pub kwx: usize, 1662 | 1663 | /// Whether initial setup has been done. 1664 | /// 1665 | /// Usually ITM will be called to get a sequence of results for varying 1666 | /// distances, and this switch gets set after the first iteration so common 1667 | /// parameters are only computed once. 1668 | pub setup_done: bool, 1669 | 1670 | pub ptx: isize, 1671 | pub los: isize, 1672 | 1673 | // statics below 1674 | pub dmin: f64, 1675 | pub xae: f64, 1676 | pub wlos: bool, 1677 | pub wscat: bool, 1678 | 1679 | pub ad: f64, 1680 | pub rr: f64, 1681 | pub etq: f64, 1682 | pub h0s: f64, 1683 | } 1684 | 1685 | /// Secondary parameters computed in LRProp. 1686 | #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] 1687 | pub struct PropA { 1688 | /// Line of sight distance 1689 | pub dlsa: f64, 1690 | 1691 | /// Scatter distance 1692 | pub dx: f64, 1693 | 1694 | /// Line of sight coefficient L 1695 | pub ael: f64, 1696 | 1697 | /// Line of sight coefficient 1 1698 | pub ak1: f64, 1699 | 1700 | /// Line of sight coefficient 2 1701 | pub ak2: f64, 1702 | 1703 | /// Diffraction coefficient 1 1704 | pub aed: f64, 1705 | 1706 | /// Diffraction coefficient 2 1707 | pub emd: f64, 1708 | 1709 | /// Scatter coefficient 1 1710 | pub aes: f64, 1711 | 1712 | /// Scatter coefficient 2 1713 | pub ems: f64, 1714 | 1715 | /// Smooth earth horizon distances (tx, rx) 1716 | pub dls: (f64, f64), 1717 | 1718 | /// Total horizon distance 1719 | pub dla: f64, 1720 | 1721 | /// Total bending angle 1722 | pub tha: f64, 1723 | } 1724 | --------------------------------------------------------------------------------