File-based I/O

LongwaveModePropagator provides basic propagation capabilities interfacing through JSON files. Julia is still needed to run the model, but scenarios can otherwise be defined and analyzed from e.g. Matlab or Python.

Examples of writing and reading compatible JSON files are provided below for Matlab and Python.

Let's load the necessary packages.

using Dates
using JSON3

using LongwaveModePropagator

Throughout the examples, we'll also define LMP as shorthand for LongwaveModePropagator when accessing functions from the package that aren't exported.

const LMP = LongwaveModePropagator

Inputs

There are two primary ways to define LongwaveModePropagator.Inputs, the abstract supertype for inputing information to the model.

ExponentialInput

The first is known as a ExponentialInput. It defines the ionosphere using Wait and Spies $h'$ and $\beta$ parameters.

It contains the fields

  • name::String
  • description::String
  • datetime::DateTime
  • segment_ranges::Vector{Float64}: distance from transmitter to the beginning of each HomogeneousWaveguide segment in meters.
  • hprimes::Vector{Float64}: Wait's $h'$ parameter for each HomogeneousWaveguide segment.
  • betas::Vector{Float64}: Wait's $\beta$ parameter for each HomogeneousWaveguide segment.
  • b_mags::Vector{Float64}: magnetic field magnitude for each HomogeneousWaveguide segment.
  • b_dips::Vector{Float64}: magnetic field dip angles in radians for each HomogeneousWaveguide segment.
  • b_azs::Vector{Float64}: magnetic field azimuth in radians "east" of the propagation direction for each HomogeneousWaveguide segment.
  • ground_sigmas::Vector{Float64}: ground conductivity in Siemens per meter for each HomogeneousWaveguide segment.
  • ground_epsrs::Vector{Int}: ground relative permittivity for each HomogeneousWaveguide segment.
  • frequency::Float64: transmitter frequency in Hertz.
  • output_ranges::Vector{Float64}: distances from the transmitter at which the field will be calculated.

The fields that are vectors allow the definition of a SegmentedWaveguide where each element of the vector is its own HomogeneousWaveguide segment. Single element vectors are treated as a single HomogeneousWaveguide.

To show the equivalent JSON format, we'll build a simple, homogeneous ionosphere ExponentialInput. It's defined as a mutable struct, so it is simple to specify the fields one by one.

input = ExponentialInput()
input.name = "basic"
input.description = "Test ExponentialInput"
input.datetime = DateTime("2020-12-29T05:00:00.000")  # usually `Dates.now()`

input.segment_ranges = [0.0]
input.hprimes = [75]
input.betas = [0.35]
input.b_mags= [50e-6]
input.b_dips = [π/2]
input.b_azs = [0.0]
input.ground_sigmas = [0.001]
input.ground_epsrs = [4]
input.frequency = 24e3
input.output_ranges = collect(0:100e3:1000e3)
11-element Vector{Float64}:
      0.0
 100000.0
 200000.0
 300000.0
 400000.0
 500000.0
 600000.0
 700000.0
 800000.0
 900000.0
      1.0e6

Here it is formatted as JSON.

json_str = JSON3.pretty(input)
{
    "name": "basic",
    "description": "Test ExponentialInput",
    "datetime": "2020-12-29T05:00:00.0",
    "segment_ranges": [
        0
    ],
    "hprimes": [
        75
    ],
    "betas": [
        0.35
    ],
    "b_mags": [
        5.0e-5
    ],
    "b_dips": [
        1.5707963267948966
    ],
    "b_azs": [
        0
    ],
    "ground_sigmas": [
        0.001
    ],
    "ground_epsrs": [
        4
    ],
    "frequency": 24000,
    "output_ranges": [
        0,
        100000,
        200000,
        300000,
        400000,
        500000,
        600000,
        700000,
        800000,
        900000,
        1000000
    ]
}

Let's also save this to a file.

json_str = JSON3.write(input)

root_dir = dirname(dirname(pathof(LongwaveModePropagator)))
examples_dir = joinpath(root_dir, "examples")
filename = joinpath(examples_dir, "basic.json")

open(filename,"w") do f
    write(f, json_str)
end

TableInput

The second input is the TableInput. This type defines the ionosphere using a tabular input of number density and collision frequency as a function of altitude. These tables are then cubic spline interpolated when integrating the ionosphere reflection coefficient and wavefields. See also interpolating functions.

The fields of the TableInput are

  • name::String
  • description::String
  • datetime::DateTime
  • segment_ranges::Vector{Float64}: distance from transmitter to the beginning of each HomogeneousWaveguide segment in meters.
  • altitude::Vector{Float64}: altitude above ground in meters for which the density and collision_frequency profiles are specified.
  • density::Vector{Float64}: electron density at each altitude in $m⁻³$.
  • collision_frequency::Vector{Float64}: electron-ion collision frequency at each altitude in $s⁻¹$.
  • b_dips::Vector{Float64}: magnetic field dip angles in radians for each HomogeneousWaveguide segment.
  • b_azs::Vector{Float64}: magnetic field azimuth in radians "east" of the propagation direction for each HomogeneousWaveguide segment.
  • ground_sigmas::Vector{Float64}: ground conductivity in Siemens per meter for each HomogeneousWaveguide segment.
  • ground_epsrs::Vector{Int}: ground relative permittivity for each HomogeneousWaveguide segment.
  • frequency::Float64: transmitter frequency in Hertz.
  • output_ranges::Vector{Float64}: distances from the transmitter at which the field will be calculated.

Again we'll construct a TableInput to look at the JSON

tinput = TableInput()
tinput.name = "table"
tinput.description = "Test TableInput"
tinput.datetime = DateTime("2020-12-29T05:00:00.000")

tinput.segment_ranges = [0.0]
tinput.altitude = collect(50e3:5e3:100e3)
tinput.density = [waitprofile.(tinput.altitude, 75, 0.3)]
tinput.collision_frequency = [electroncollisionfrequency.(tinput.altitude)]
tinput.b_mags= [50e-6]
tinput.b_dips = [π/2]
tinput.b_azs = [0.0]
tinput.ground_sigmas = [0.001]
tinput.ground_epsrs = [4]
tinput.frequency = 24e3
tinput.output_ranges = collect(0:100e3:1000e3)

json_str = JSON3.pretty(tinput)
{
    "name": "table",
    "description": "Test TableInput",
    "datetime": "2020-12-29T05:00:00.0",
    "segment_ranges": [
        0
    ],
    "altitude": [
        50000,
        55000,
        60000,
        65000,
        70000,
        75000,
        80000,
        85000,
        90000,
        95000,
        100000
    ],
    "density": [
        [
            4.374403183176109e6,
            9.26061161145436e6,
            1.9604714935292408e7,
            4.150318184370078e7,
            8.78622366525934e7,
            1.8600435645316696e8,
            3.9377122570138437e8,
            8.33613691351424e8,
            1.7647601984395177e9,
            3.735997369413846e9,
            7.909106493114021e9
        ]
    ],
    "collision_frequency": [
        [
            1.0044012161884667e8,
            4.744455400598282e7,
            2.2411220422141008e7,
            1.0586310933525776e7,
            5.0006192019140925e6,
            2.362125253978684e6,
            1.1157889633644049e6,
            527061.3862109144,
            248966.17008735015,
            117603.29151329474,
            55551.86140313166
        ]
    ],
    "b_mags": [
        5.0e-5
    ],
    "b_dips": [
        1.5707963267948966
    ],
    "b_azs": [
        0
    ],
    "ground_sigmas": [
        0.001
    ],
    "ground_epsrs": [
        4
    ],
    "frequency": 24000,
    "output_ranges": [
        0,
        100000,
        200000,
        300000,
        400000,
        500000,
        600000,
        700000,
        800000,
        900000,
        1000000
    ]
}

BatchInput

Both the ExponentialInput and TableInput types can be collected together in a BatchInput which has fields for a name, description, datetime, and vector of Inputs. This is useful for keeping a set of scenarios together. See the test/IO.jl file for additional help on how these should be formatted.

Running the model from a JSON file

To run the model, propagate accepts a filename input (see the help for optional arguments). However, instead of returning a tuple of complex electric field, amplitude, and phase, it returns an Output type. Additionally, it saves the output to a JSON file.

output = propagate(filename);

Outputs

There are only two LongwaveModePropagator.Output types: BasicOutput and BatchOutput. Both ExponentialInputs and TableInputs create BasicOutputs, but the BatchInput creates a BatchOutput.

The BasicOutput contains fields for

  • name::String
  • description::String
  • datetime::DateTime
  • output_ranges::Vector{Float64}
  • amplitude::Vector{Float64}
  • phase::Vector{Float64}

where name and description are directly copied from the input and datetime is when the model was run.

output
BasicOutput("basic", "Test ExponentialInput", Dates.DateTime("2024-03-16T18:39:28.443"), [0.0, 100000.0, 200000.0, 300000.0, 400000.0, 500000.0, 600000.0, 700000.0, 800000.0, 900000.0, 1.0e6], [0.0, 69.0420085588333, 63.22947398509886, 60.327867672184254, 52.46540662748447, 56.88153291599398, 54.30208286924061, 47.3461069885161, 40.45356838712788, 40.881809048792746, 43.81414359827729], [0.7853981633974483, 1.9976603131585595, 1.7444723978632044, 1.6653810259804087, 1.087231658038243, 1.2321020702529828, 1.6640892911327365, 2.0612234882159335, 2.6906710175389246, 4.465406859525416, 5.067125999764023])

Not surprisingly, a BatchOutput is simply a container holding a Vector of BasicOutputs, as well as some additional metadata from the corresponding BatchInput.

JSON I/O from Matlab

Here's an example of how to encode the above ExponentialInput to JSON and decode the output using Matlab. It's also in the file examples/io.m.

% Matlab script

input.name = "basic";
input.description = "Test ExponentialInput";
input.datetime = '2020-12-28T21:06:50.501';

input.segment_ranges = {0.0};
input.hprimes = {75};
input.betas = {0.35};
input.b_mags = {50e-6};
input.b_dips = {pi/2};
input.b_azs = {0.0};
input.ground_sigmas = {0.001};
input.ground_epsrs = {4};
input.frequency = 24e3;
input.output_ranges = 0:100e3:1000e3;

json_str = jsonencode(input);

fid = fopen('basic_matlab.json', 'w');
fwrite(fid, json_str, 'char');
fclose(fid);

Matlab was used to generate the file basic_matlab.json. We can confirm it's parsed correctly by using the internal LongwaveModePropagator function LongwaveModePropagator.parse, which attempts to parse JSON files into recognized input and output formats.

matlab_input = LMP.parse(joinpath(examples_dir, "basic_matlab.json"))
ExponentialInput("basic", "Test ExponentialInput", Dates.DateTime("2020-12-28T21:06:50.501"), [0.0], [75.0], [0.35], [5.0e-5], [1.5707963267948966], [0.0], [0.001], [4], 24000.0, [0.0, 100000.0, 200000.0, 300000.0, 400000.0, 500000.0, 600000.0, 700000.0, 800000.0, 900000.0, 1.0e6])

Let's run it.

matlab_output = propagate(joinpath(examples_dir, "basic_matlab.json"));
Note

It is possible to run Julia as a script, calling it directly from a terminal (see the docs), but it is probably easiest to just run the code from the REPL.

  1. cd in a terminal to your working directory
  2. Start up Julia: julia
  3. It's recommended to ] activate ., generating a new environment in this directory
  4. If necessary, install LongwaveModePropagator.jl: ] add LongwaveModePropagator
  5. using LongwaveModePropagator
  6. propagate("basic_matlab.json")

If this has been done before in the same directory, then just do steps 1, 2, 5, 6.

Reading the results file from Matlab is relatively simple.

% Matlab script

filename = 'basic_matlab_output.json';

text = fileread(filename);

output = jsondecode(text);

JSON I/O from Python

Here is similar code for Python, also available in the file io.py.

# Python script

import json
import datetime
import numpy as np

input = dict()

input['name'] = "basic"
input['description'] = "Test ExponentialInput"
input['datetime'] = datetime.datetime.now().isoformat()[:-3]

input['segment_ranges'] = [0.0]
input['hprimes'] = [75]
input['betas'] = [0.35]
input['b_mags'] = [50e-6]
input['b_dips'] = [np.pi/2]
input['b_azs'] = [0.0]
input['ground_sigmas'] = [0.001]
input['ground_epsrs'] = [4]
input['frequency'] = 24e3
input['output_ranges'] = np.arange(0, 1000e3, 100e3).tolist()

json_str = json.dumps(input)

with open('basic_python.json', 'w') as file:
    file.write(json_str)

Let's ensure that the JSON file is correctly parsed.

python_input = LMP.parse(joinpath(examples_dir, "basic_python.json"))
ExponentialInput("basic", "Test ExponentialInput", Dates.DateTime("2020-12-29T11:35:11.989"), [0.0], [75.0], [0.35], [5.0e-5], [1.5707963267948966], [0.0], [0.001], [4], 24000.0, [0.0, 100000.0, 200000.0, 300000.0, 400000.0, 500000.0, 600000.0, 700000.0, 800000.0, 900000.0])

And run the file.

python_output = propagate(joinpath(examples_dir, "basic_python.json"));

To read the results:

with open('basic_python_output.json', 'r') as file:
    output = json.load(file)

This page was generated using Literate.jl.