From 8a2c630ac729fbd745f078e51911a3cb77e4e774 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 15:26:34 +0200 Subject: [PATCH 01/16] First post-processing functions Some functions to post-process LaMEM models. Plotting functions are in progress. --- docs/src/man/lamem_post_processing.md | 15 + .../src/man/tutorial_lamem_post_processing.md | 61 ++ src/LaMEM_post_processing.jl | 532 ++++++++++++++++++ test/test_LaMEM_post_processing.jl | 64 +++ .../output.pvtr | 25 + .../output_phase.pvtr | 16 + .../output.pvtr | 25 + .../output_phase.pvtr | 16 + .../trackCartesianIndex(10, 10, 10).txt | Bin 0 -> 232 bytes tutorials/Tutorial_post_processing.jl | 40 ++ 10 files changed, 794 insertions(+) create mode 100644 docs/src/man/lamem_post_processing.md create mode 100644 docs/src/man/tutorial_lamem_post_processing.md create mode 100644 src/LaMEM_post_processing.jl create mode 100644 test/test_LaMEM_post_processing.jl create mode 100644 test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr create mode 100644 test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr create mode 100644 test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr create mode 100644 test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr create mode 100644 test/test_files/timestep/trackCartesianIndex(10, 10, 10).txt create mode 100644 tutorials/Tutorial_post_processing.jl diff --git a/docs/src/man/lamem_post_processing.md b/docs/src/man/lamem_post_processing.md new file mode 100644 index 000000000..3729b7b2f --- /dev/null +++ b/docs/src/man/lamem_post_processing.md @@ -0,0 +1,15 @@ +# Post processing of numerical models + +To evaluate and analyse numerical simulations, we provide a few routines to make it easier to extract the dataset information. + + +```@docs +get_phase +get_phase_bool +search_for_phase_properties +get_data_timestep +split_at__to_type +search_for_model_constrains +track_point_over_time +deserialize_file +``` \ No newline at end of file diff --git a/docs/src/man/tutorial_lamem_post_processing.md b/docs/src/man/tutorial_lamem_post_processing.md new file mode 100644 index 000000000..0da681cb7 --- /dev/null +++ b/docs/src/man/tutorial_lamem_post_processing.md @@ -0,0 +1,61 @@ +# Post processing of LaMEM files + +## Goal +This tutorial visualizes how to do comparative analysis and quantitative evaluation of LaMEM models. The post-processing is julia based and extracts the information directly from the .pvtr-file. + + +## Steps + +## 1. Get general information +Before beginning the post-processing, it is useful to extract some general information about the model. This includes material properties of the different phases, time of the time step, ascii-file stored information and the model data. + + +```julia +using GeophysicalModelGenerator, LaMEM, Serialization + +#### set path and variables +dat_path = ("../test/test_files/Subduction_VEP.dat") +model_path = ("../test/test_files/timestep/") +model_name = "VEP" +timestep = "Timestep_00000000_0.00000000e+00" +FileName_pvtr = "output.pvtr" +p_fields = ["phase", "temperature"] +output_dir = model_path + +# extract data information from ascii file +surface_level = search_for_model_constrains(dat_path, "surf_level") + +# read output file +material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +material_block = search_for_phase_properties(dat_path, model_name, "", "") + +# get the time as a float number +time = split_at__to_type([timestep],3,"Float") + + +# extract data information for selected fields +data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,false) +``` + +## 2. Start post-processing +To analyse the differences between models and its evolution, coordinates of specific phases can be obtained either as a vector or as a matrix. Additionally, a point can be tracked over time for specific fields. + + +```julia +# get information about where the phase is located +processing_folder = joinpath(model_path,timestep) +path = replace(processing_folder,"\\" => "/")*"/" +indices = get_phase(path,FileName_pvtr,[2],false) +matrix = get_phase_bool(path,FileName_pvtr,indices) + +# track one point over time +name = "track"*string(indices[1]) +track_point = track_point_over_time(indices[1],p_fields,model_name,model_path,surface_level,name,output_dir,false) + +tracked_point = deserialize_file(output_dir,name) +``` + + + + +If you want to run the entire example, you can find the .jl code [here](https://github.com/JuliaGeodynamics/GeophysicalModelGenerator.jl/blob/main/tutorial/Tutorial_post_processing.jl) \ No newline at end of file diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl new file mode 100644 index 000000000..f14310b14 --- /dev/null +++ b/src/LaMEM_post_processing.jl @@ -0,0 +1,532 @@ +using Unitful +using NaturalSort +using GeophysicalModelGenerator +using LaMEM +using Serialization + + + + +export search_for_phase_properties, get_phase_number, search_for_model_constrains, get_phase, +get_phase_bool, get_data_timestep, split_at__to_type, track_point_over_time, deserialize_file + + +""" + material_block = search_for_phase_properties(dat_file::String=dat_path, model_name::String = model_name, search_name::String="", stop_name::String="") + +Parameters +==== +- `dat_file` - path to the .dat file in the model folder +- `model_name` - name of the model +- `search_name` - searching string in dat file +- `stop_name` - stopping string in dat file + + +Examples +======== + +```julia + +julia> dat_path = ("test/test_files/Subduction_VEP.dat") +julia> model_name = "VEP" +julia> material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +julia> material_block = search_for_phase_properties(dat_path, model_name, "", "") + +Dict{String, Dict{String, Dict{String, String}}} with 1 entry: + "VEP" => Dict("Phase5"=>Dict("ch"=>"20e6 # cohesion [Pa]", "Cp"=>"1.2e3 # heat capacity", "fr"=>"30 # friction angle [deg]", "k"=>"2.5", "alpha"=>"1e-5", "disl_prof"=>"Dry_Olivine_disl_creep… + + ``` +""" + + + +# Search for a phase in a file and store the properties + +function search_for_phase_properties(dat_file::String, model_name::String, search_name::String, stop_name::String) + number_lines = Int[] # To store line numbers where search_name is found + stop_lines = Int[] # To store line numbers where stop_name is found + + + # Open the file and read lines + lines = String[] + open(dat_file, "r") do file + lines = readlines(file) # Read all lines at once + end + + # Search for 'search_name' and 'stop_name' in the file + for i in eachindex(lines) + if occursin(search_name, lines[i]) + push!(number_lines, i) + end + if occursin(stop_name, lines[i]) + push!(stop_lines, i) + end + end + + # Initialize material_block entry for the folder if it doesn't exist + if !haskey(material_block, model_name) + material_block[model_name] = Dict() + end + + for k in eachindex(number_lines) + # Create a phase entry within the folder + phase_key = "Phase$(k-1)" # e.g., "Phase0", "Phase1", etc. + if !haskey(material_block[model_name], phase_key) + material_block[model_name][phase_key] = Dict() # Initialize phase in the folder + end + + # Iterate through the lines between number_lines[k] and stop_lines[k] + for i in number_lines[k]+1:stop_lines[k]-1 + line = strip(lines[i]) # Clean the line by removing extra spaces + if occursin("=", line) # Make sure the line contains an '=' sign + parts = split(line, "=") # Split at the '=' symbol + if length(parts) == 2 + property_name = strip(parts[1]) # Get the property name + property_value = strip(parts[2]) # Get the property value + # Store in dictionary, ensure we're adding properties correctly + material_block[model_name][phase_key][property_name] = property_value + end + end + end + end + + return material_block +end + +########################################################################################### + + +""" + search_value = search_for_model_constrains(path::String, search_name::String) + +Parameters +==== +- `path` - path to the ascii file in the model folder +- `search_name` - searching string in the ascii file + + +Examples +======== + +```julia + +julia> dat_path = ("test/test_files/Subduction_VEP.dat") +julia> search_name = "surf_level" +julia> surface_level = search_for_model_constrains(dat_path, search_name) + +1-element Vector{Any}: + 0.0 + + ``` +""" + +# search for values set in the julia file to create and are written to ascii files. + +function search_for_model_constrains(path::String, search_name::String) + number_lines = Int[] # To store line numbers where search_name is found + model_constrain=[] + + # Open the file and read lines + lines = open(path, "r") do file + readlines(file) # Read all lines at once + end + + # Search for 'search_name' and 'stop_name' in the file + for i in eachindex(lines) + if occursin(search_name, lines[i]) + push!(number_lines, i) + con_l = [m.match for m in eachmatch(r"(\d+\.?\d*)", lines[i])] + for num in con_l + push!(model_constrain, parse(Float64, num)) + end + break + end + end + + return model_constrain +end + +############################################################################## + + +""" + phase_coord = get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ind=false) + +Parameters +==== +- `path` - path to pvtr file +- `FileName_pvtr` - file name if the pvtr file +- `phaseIDs` - number of phase which should be detected +- `sep_ind` - false: if each phase coordinates should be stored separately or true: if all coordinates in one vector + + +Examples +======== + +```julia + +julia> path = ("test/test_files/timestep/") +julia> FileName_pvtr = "output.pvtr" +julia> phase_coord = get_phase(path,FileName_pvtr,[2],false) + + +343-element Vector{CartesianIndex{3}}: + CartesianIndex(10, 10, 10) + CartesianIndex(11, 10, 10) + CartesianIndex(12, 10, 10) + CartesianIndex(13, 10, 10) + CartesianIndex(14, 10, 10) + + ``` +""" + + + +# get coordinates of one specific phase + +function get_phase(path::String,FileName_pvtr::String,phaseID::Int) + + indices = [] + + # processing folder + proc_folder = replace(path,"\\" => "/")*"/" + println("Processing folder:" * proc_folder) + + data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr;fields="phase") + + # Get the indices of the phase in the current row + ind = findall(x -> x == Float64(phaseID), data.fields.phase) + indices = push!(indices, ind) + + return indices +end + +########################################################################################################################################### + +# get Coordinates of the searched phases. Return can either be in one list or separated by phase than as vector{vectors} +function get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ind=false) + + indices = [] + idx = [] + + if length(phaseIDs) == 1 + sep_ind=false + ind = get_phase(path,FileName_pvtr,phaseIDs[1]) + indices = ind[1] + else + # Get the indices of the phase in the current row + for i in 1:length(phaseIDs) + + ind = get_phase(path,FileName_pvtr,i) + indices = push!(indices, ind) + + if (sep_ind == true && i > 1) + idx = vcat(indices[i-1], indices[i]) + end + + end + end + + if sep_ind == true + indices = idx[1] + end + + return indices +end + +#################################################################################### + +""" + matrix = get_phase_bool(path::String,FileName_pvtr::String,ind) + +Parameters +==== +- `path` - path to pvtr file +- `FileName_pvtr` - file name if the pvtr file +- `ind` - CartesianIndex Points where the phase is detected + + +Examples +======== + +```julia + +julia> path = ("test/test_files/timestep/") +julia> FileName_pvtr = "output.pvtr" +julia> phase_coord = get_phase(path,FileName_pvtr,[2],false) + +julia> matrix = get_phase_bool(path,FileName_pvtr,phase_coord) + +33×33×33 Array{Int64, 3}: +[:, :, 1] = + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 .... + + ``` +""" + +# boolse matrix, where coordinates of phase exist values are set to true +function get_phase_bool(path::String,FileName_pvtr::String,ind) + + idx = [] + proc_folder = replace(path,"\\" => "/")*"/" + data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr) + if any(x -> isa(x, Vector), ind) + for i in eachindex(ind) + if i > 1 + idx = vcat(ind[i-1], ind[i]) + end + end + ind = idx[1] + end + + matrix =zeros(size(data.x.val)) + matrix[ind[:]] .= 1 + matrix = Int64.(matrix) + + return matrix +end + + +############################################################################## + + +""" + data = get_data_timestep(model_path::String,timestep::String,FileName_pvtr::String,p_fields::Vector{String},surface_level::Vector{Any},print=false) + +Parameters +==== +- `model_path` - path to time steps +- `timestep` - time step folder name +- `FileName_pvtr` - file name of the pvtr file +- `p_fields` - property fields which should be read +- `surface_level` - get surface level for depth correction +- `print` - if true prints current processing time step + + +Examples +======== + +```julia + +julia> model_path = "test/test_files/timestep/" +julia> dat_path = "test/test_files/Subduction_VEP.dat" +julia> timestep = "Timestep_00000000_0.00000000e+00" +julia> FileName_pvtr = "output.pvtr" +julia> p_fields = ["phase", "temperature"] +julia> surface_level = search_for_model_constrains(dat_path, "surf_level") + +julia> data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,print=false) + +CartData + size : (33, 33, 33) + x ϵ [ 0.0 : 1.0] + y ϵ [ 0.0 : 1.0] + z ϵ [ 0.0 : 1.0] + fields : (:phase, :temperature) + + ``` +""" + + +# reads data of one time step and corrects the surface level + +function get_data_timestep(model_path::String,timestep::String,FileName_pvtr::String,p_fields::Vector{String},surface_level::Vector{Any},print=false) + # processing folder + processing_folder = joinpath(model_path,timestep) + proc_folder = replace(processing_folder,"\\" => "/")*"/" + + if print == true + println("Processing folder:" * proc_folder) + else + nothing + end + + data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr;fields=p_fields) + + # Correct surface level + data.z.val .= data.z.val .- surface_level # surf_level read from dat file + + return data +end + + +############################################################################################ + +""" + split_entry = split_at__to_type(string_to_split::Vector{String} = ["Timestep_00000000_0.00000000e+00"],i::Int64 = 2,type::String="Float") + +Parameters +==== +- `string_to_split` - string which will be split at _ +- `i` - position within the string which will be returned +- `type` - select type how it will be return: Int, Float, or String + + +Examples +======== + +```julia + +julia> string_to_split = ["Timestep_00000000_0.00000000e+00"] +julia> split_value = split_at__to_type(string_to_split,2,"Float") + +1-element Vector{Float64}: + 0.0 + + ``` +""" + +# extract time from time_files + +function split_at__to_type(string_to_split::Vector{String},i::Int64,type::String) + + if occursin("String", type) + spl = [split(entry, '_')[i] for entry in string_to_split] + elseif occursin("Float", type) + spl = [parse(Float64, split(entry, '_')[i]) for entry in string_to_split] + elseif occursin("Int", type) + spl = [parse(Int64, split(entry, '_')[i]) for entry in string_to_split] + end + return spl + +end + + + + +#################################################################################################################### + +""" + data = track_point_over_time(Point_coord::CartesianIndex,fields::Vector{String},model_name::String,timefile_location::String,surface_level::Vector, name::String, output_dir::String,save = false) + + +Parameters +==== +- `Point_coord` - tracked point in CartesianIndex +- `fields` - property fields which should be tracked +- `timefile_location` - path of the time step +- `surface_level` - get surface level for depth correction +- `name` - saving file name +- `output_dir` - save location +- `save` - true: saved, false: not saved + + +Examples +======== + +```julia + +julia> Point_coord = CartesianIndex(10, 10, 10) +julia> model_name = "PEV" +julia> timefile_location = "test/test_files/timestep/" +julia> timestep = "Timestep_00000000_0.00000000e+00" +julia> p_fields = ["phase", "temperature"] +julia> surface_level = search_for_model_constrains(dat_path, "surf_level") +julia> name = "track"*string(CartesianIndex(200,1,200)) +julia> output_dir = "test/test_files/timestep/" + +julia> track_point = track_point_over_time(Point_coord,p_fields,model_name,timefile_location,surface_level, name::String, output_dir::String,save = false) + +Dict{String, Dict{String, Float64}} with 2 entries: + "Timestep_00000010_1.49079279e+01" => Dict("phase"=>1.53445, "temperature"=>0.0) + "Timestep_00000000_0.00000000e+00" => Dict("phase"=>2.0, "temperature"=>0.0) + ... + ``` +""" + + + +# track one specific point over time for multiple fields + +function track_point_over_time(Point_coord::CartesianIndex,fields::Vector{String},model_name::String,timefile_location::String,surface_level::Vector, name::String, output_dir::String,save = false) + + track_point = Dict{String,Dict{String,Float64}}() + + time_files = filter(f -> startswith(f, "Time"), readdir(timefile_location)) + save_dict = Dict{String,Dict{String,Dict{String,Float64}}}() + + for timestep in time_files + + data = get_data_timestep(timefile_location,timestep,FileName_pvtr,fields,surface_level,false) + track_point[timestep] = Dict{String,Float64}() + + for field in fields + + field_name = Symbol(field) + field_data = getfield(data.fields, field_name) + + track_point[timestep][field] = field_data[Point_coord.I[1],Point_coord.I[2],Point_coord.I[3]] + + end + end + + + if save == true + save_dict[model_name] = Dict{String,Dict{String,Float64}}() + save_dict[model_name] = track_point + + file_name = string(name)*".txt" + output_name=joinpath(output_dir,file_name) + + # Serialize the vector of structures to a file + open(output_name, "a") do file + serialize(file, save_dict) + end + end + + + return track_point + +end + + +################################################################################################################### + +""" + file = deserialize_file(output_dir::String,name::String) + +Parameters +==== +- `output_dir` - directory of the file +- `name` - name of the file + + +Examples +======== + +```julia + +julia> output_dir = "./output/ +julia> name = "track"*string(CartesianIndex(10,10,10)) +julia> file_info = deserialize_file(output_dir,name) + +1-element Vector{Any}: + Dict("VEP" => Dict("Timestep_00000010_1.49079279e+01" => Dict("phase" => 1.534447431564331, "temperature" => 0.0), + "Timestep_00000000_0.00000000e+00" => Dict("phase" => 2.0, "temperature" => 0.0))) + + ``` +""" + + + +# load and extract information of serialized files + +function deserialize_file(output_dir::String,name::String) + + file_name = string(name)*".txt" + output_name=joinpath(output_dir,file_name) + + det_info =[] + + open(output_name, "r") do file + while !eof(file) + loaded_detach_instances = deserialize(file) + push!(det_info, loaded_detach_instances) + end + end + + return det_info +end + + diff --git a/test/test_LaMEM_post_processing.jl b/test/test_LaMEM_post_processing.jl new file mode 100644 index 000000000..bed11ef55 --- /dev/null +++ b/test/test_LaMEM_post_processing.jl @@ -0,0 +1,64 @@ +using GeophysicalModelGenerator, LaMEM, Serialization, Test + + +# load data file +dat_path = ("./test_files/Subduction_VEP.dat") +model_path = ("./test_files/timestep/") +model_name = "VEP" +timestep = "Timestep_00000000_0.00000000e+00" +FileName_pvtr = "output.pvtr" +p_fields = ["phase", "temperature"] +output_dir = model_path + +# test extraction from ascii files +# extract data information from data file +surface_level = search_for_model_constrains(dat_path, "surf_level") +gravity = search_for_model_constrains(dat_path, "gravity") + +@test surface_level == [0.0] +@test gravity == [0.0, 0.0, 10.0] + +# read output file +material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +material_block = search_for_phase_properties(dat_path, model_name, "", "") + +@test material_block[model_name]["Phase5"]["fr"] == "30 # friction angle [deg]" +@test material_block[model_name]["Phase0"]["rho"] == "100" + + +# test time extraction +# get the time as a float number +time3 = split_at__to_type([timestep],3,"Float") +time2 = split_at__to_type([timestep],2,"Int") +@test time3 == [0.0] +@test time2 == [0] + +# get information about where the phase is located +processing_folder = joinpath(model_path,timestep) +path = replace(processing_folder,"\\" => "/")*"/" +indices = get_phase(path,FileName_pvtr,[2],false) +matrix = get_phase_bool(path,FileName_pvtr,indices) +@test sum(matrix) == 343 +@test length(indices) == 343 +@test matrix[indices[1]] == 1 +@test matrix[CartesianIndex(1,1,1)] == 0 + + +# extract data information +pvtr_path = joinpath(model_path,timestep)*"/" +data_pvtr = read_LaMEM_PVTR_file(pvtr_path,FileName_pvtr;fields = p_fields) +data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,false) + +@test (getindex(data.z[1,1,1])) == (getindex(data_pvtr.z[1,1,1])) + + +# track one point over time +name = "track"*string(indices[1]) +track_point = track_point_over_time(indices[1],p_fields,model_name,model_path,surface_level,name,output_dir,false) + +@test track_point["Timestep_00000010_1.49079279e+01"]["phase"] == 1.534447431564331 +@test track_point["Timestep_00000000_0.00000000e+00"]["phase"] == 2.0 + + +tracked_point = deserialize_file(output_dir,name) +@test first(keys(track_point)) == "Timestep_00000010_1.49079279e+01" \ No newline at end of file diff --git a/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr b/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr new file mode 100644 index 000000000..9816ed3c5 --- /dev/null +++ b/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr b/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr new file mode 100644 index 000000000..e31cf0615 --- /dev/null +++ b/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr b/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr new file mode 100644 index 000000000..9816ed3c5 --- /dev/null +++ b/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr b/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr new file mode 100644 index 000000000..e31cf0615 --- /dev/null +++ b/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/test_files/timestep/trackCartesianIndex(10, 10, 10).txt b/test/test_files/timestep/trackCartesianIndex(10, 10, 10).txt new file mode 100644 index 0000000000000000000000000000000000000000..e5b46c44e978a74bdc20e2601bcda7cd268dd0b0 GIT binary patch literal 232 zcmXr_@{wR+U|=v6U}SO0OfHf4o9Ds=km+Cm0Qko%D*ylh literal 0 HcmV?d00001 diff --git a/tutorials/Tutorial_post_processing.jl b/tutorials/Tutorial_post_processing.jl new file mode 100644 index 000000000..aff552537 --- /dev/null +++ b/tutorials/Tutorial_post_processing.jl @@ -0,0 +1,40 @@ +using GeophysicalModelGenerator, LaMEM, Serialization + +# load data file +dat_path = ("../test/test_files/Subduction_VEP.dat") +model_path = ("../test/test_files/timestep/") +model_name = "VEP" +timestep = "Timestep_00000000_0.00000000e+00" +FileName_pvtr = "output.pvtr" +p_fields = ["phase", "temperature"] +output_dir = model_path + +# extract data information from data file +surface_level = search_for_model_constrains(dat_path, "surf_level") + +# read output file +material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +material_block = search_for_phase_properties(dat_path, model_name, "", "") + +# get the time as a float number +time = split_at__to_type([timestep],3,"Float") + +# extract data information +data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,false) + +# get information about where the phase is located +processing_folder = joinpath(model_path,timestep) +path = replace(processing_folder,"\\" => "/")*"/" +indices = get_phase(path,FileName_pvtr,[2],false) +matrix = get_phase_bool(path,FileName_pvtr,indices) + +# track one point over time +name = "track"*string(indices[1]) +track_point = track_point_over_time(indices[1],p_fields,model_name,model_path,surface_level,name,output_dir,false) + +tracked_point = deserialize_file(output_dir,name) + + + + + From a73abbc2a8861de422c4b865bec27d08fb6727a6 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:36:45 +0200 Subject: [PATCH 02/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index f14310b14..fb65c27fb 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -206,8 +206,8 @@ end # get Coordinates of the searched phases. Return can either be in one list or separated by phase than as vector{vectors} function get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ind=false) - indices = [] - idx = [] + indices = Int64[] + idx = Int64[] if length(phaseIDs) == 1 sep_ind=false From ad6b454509009f35569fb85deaf89fdaa3c6eb55 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:37:00 +0200 Subject: [PATCH 03/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index fb65c27fb..5403ed7c4 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -215,7 +215,7 @@ function get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ indices = ind[1] else # Get the indices of the phase in the current row - for i in 1:length(phaseIDs) + for i in eachindex(phaseIDs) ind = get_phase(path,FileName_pvtr,i) indices = push!(indices, ind) From dcaff594b50a29e34d66380788cad1688e065ec3 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:37:23 +0200 Subject: [PATCH 04/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index 5403ed7c4..f1893deed 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -220,7 +220,7 @@ function get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ ind = get_phase(path,FileName_pvtr,i) indices = push!(indices, ind) - if (sep_ind == true && i > 1) + if sep_ind && i > 1 idx = vcat(indices[i-1], indices[i]) end From 196fa9f617fb95ca26989f87f6a22d01d9144cab Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:37:33 +0200 Subject: [PATCH 05/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index f1893deed..ac678c5ad 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -227,7 +227,7 @@ function get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ end end - if sep_ind == true + if sep_ind indices = idx[1] end From 4166f4ccee697fd865f22a2eea0ef8d75e96c7a9 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:37:44 +0200 Subject: [PATCH 06/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index ac678c5ad..891111adb 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -268,7 +268,7 @@ julia> matrix = get_phase_bool(path,FileName_pvtr,phase_coord) # boolse matrix, where coordinates of phase exist values are set to true function get_phase_bool(path::String,FileName_pvtr::String,ind) - idx = [] + idx = Int64[] proc_folder = replace(path,"\\" => "/")*"/" data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr) if any(x -> isa(x, Vector), ind) From 323fe37f457347486fd2895acf6e46d721626862 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:38:29 +0200 Subject: [PATCH 07/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index 891111adb..ad047393a 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -336,11 +336,7 @@ function get_data_timestep(model_path::String,timestep::String,FileName_pvtr::St processing_folder = joinpath(model_path,timestep) proc_folder = replace(processing_folder,"\\" => "/")*"/" - if print == true - println("Processing folder:" * proc_folder) - else - nothing - end + print && println("Processing folder:" * proc_folder) data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr;fields=p_fields) From 6f37dfce328d1cd966c7a940b19b1fd58f901e5c Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:38:43 +0200 Subject: [PATCH 08/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index ad047393a..274480e63 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -341,7 +341,7 @@ function get_data_timestep(model_path::String,timestep::String,FileName_pvtr::St data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr;fields=p_fields) # Correct surface level - data.z.val .= data.z.val .- surface_level # surf_level read from dat file + data.z.val .-= surface_level # surf_level read from dat file return data end From a349d06254de2f8d76dcc7ae45cae8ddc07eaf77 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:39:03 +0200 Subject: [PATCH 09/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index 274480e63..9d6fb8182 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -377,12 +377,12 @@ julia> split_value = split_at__to_type(string_to_split,2,"Float") function split_at__to_type(string_to_split::Vector{String},i::Int64,type::String) - if occursin("String", type) - spl = [split(entry, '_')[i] for entry in string_to_split] + spl = if occursin("String", type) + [split(entry, '_')[i] for entry in string_to_split] elseif occursin("Float", type) - spl = [parse(Float64, split(entry, '_')[i]) for entry in string_to_split] + [parse(Float64, split(entry, '_')[i]) for entry in string_to_split] elseif occursin("Int", type) - spl = [parse(Int64, split(entry, '_')[i]) for entry in string_to_split] + [parse(Int64, split(entry, '_')[i]) for entry in string_to_split] end return spl From 2eb65c938a12109a8845b111f67cec70aa196136 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:39:16 +0200 Subject: [PATCH 10/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index 9d6fb8182..99a475d79 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -458,7 +458,7 @@ function track_point_over_time(Point_coord::CartesianIndex,fields::Vector{String end - if save == true + if save save_dict[model_name] = Dict{String,Dict{String,Float64}}() save_dict[model_name] = track_point From 096150d3a18d22ef616cd138ba9e2ef2cbcc0a71 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:39:36 +0200 Subject: [PATCH 11/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index 99a475d79..d9c6f15e2 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -510,7 +510,7 @@ julia> file_info = deserialize_file(output_dir,name) function deserialize_file(output_dir::String,name::String) - file_name = string(name)*".txt" + file_name = name * ".txt" output_name=joinpath(output_dir,file_name) det_info =[] From 714255dc2ede66b38ec4ce2c21d21006b8528f01 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 17:41:25 +0200 Subject: [PATCH 12/16] Update src/LaMEM_post_processing.jl Co-authored-by: Albert de Montserrat <58044444+albert-de-montserrat@users.noreply.github.com> --- src/LaMEM_post_processing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index d9c6f15e2..bc1b5fab9 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -281,7 +281,7 @@ function get_phase_bool(path::String,FileName_pvtr::String,ind) end matrix =zeros(size(data.x.val)) - matrix[ind[:]] .= 1 + @views matrix[ind[:]] .= 1 matrix = Int64.(matrix) return matrix From c9bee8a2ace617ad577419e40de6b5bbb1cd97b0 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Wed, 7 May 2025 23:01:06 +0200 Subject: [PATCH 13/16] Update runtests.jl --- test/runtests.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index e6438262f..39a947fb6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -81,6 +81,10 @@ using Test include("test_ASAGI_IO.jl") end + @testset "LaMEM_post_processing" begin + include("test_LaMEM_post_processing.jl") + end + end # Cleanup From 1358775eca0f55b8ed1116b6bf6bd3ed3a46739c Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:01:30 +0200 Subject: [PATCH 14/16] Post processing LaMEM and add_poly --- docs/src/man/lamem_post_processing.md | 9 +- .../src/man/tutorial_lamem_post_processing.md | 94 +- src/GeophysicalModelGenerator.jl | 1 - src/LaMEM_post_processing.jl | 1288 +++++++++++++++-- src/Setup_geometry.jl | 4 +- test/runtests.jl | 6 +- test/test_LaMEM_post_processing.jl | 105 +- .../output.pvtr | 33 + .../output_passive_tracers.pvtu | 25 + .../output_surf.pvts | 19 + test/test_files/output/phase/Fig0_-2.5.png | Bin 0 -> 82173 bytes .../output/temperature/Fig0_-2.5.png | Bin 0 -> 101819 bytes test/test_lamem.jl | 120 -- tutorials/Tutorial_post_processing.jl | 69 +- 14 files changed, 1463 insertions(+), 310 deletions(-) create mode 100644 test/test_files/Timestep_00000000_0.00000000e+00/output.pvtr create mode 100644 test/test_files/Timestep_00000000_0.00000000e+00/output_passive_tracers.pvtu create mode 100644 test/test_files/Timestep_00000000_0.00000000e+00/output_surf.pvts create mode 100644 test/test_files/output/phase/Fig0_-2.5.png create mode 100644 test/test_files/output/temperature/Fig0_-2.5.png delete mode 100644 test/test_lamem.jl diff --git a/docs/src/man/lamem_post_processing.md b/docs/src/man/lamem_post_processing.md index 3729b7b2f..72c2b5202 100644 --- a/docs/src/man/lamem_post_processing.md +++ b/docs/src/man/lamem_post_processing.md @@ -8,8 +8,15 @@ get_phase get_phase_bool search_for_phase_properties get_data_timestep +get_tracer_timestep +get_surf_timestep split_at__to_type search_for_model_constrains +search_for_all_model_constrains +post_plot +find_field_properties_grid +find_general_grid_prop +find_surf_evolution +find_tracer_info track_point_over_time -deserialize_file ``` \ No newline at end of file diff --git a/docs/src/man/tutorial_lamem_post_processing.md b/docs/src/man/tutorial_lamem_post_processing.md index 0da681cb7..a8b73bea4 100644 --- a/docs/src/man/tutorial_lamem_post_processing.md +++ b/docs/src/man/tutorial_lamem_post_processing.md @@ -1,44 +1,52 @@ # Post processing of LaMEM files ## Goal -This tutorial visualizes how to do comparative analysis and quantitative evaluation of LaMEM models. The post-processing is julia based and extracts the information directly from the .pvtr-file. +This tutorial visualizes how to do comparative analysis and quantitative evaluation of LaMEM models. The post-processing is julia based and extracts the information directly from the pvtr-, pvts- and pvtu-files. ## Steps ## 1. Get general information -Before beginning the post-processing, it is useful to extract some general information about the model. This includes material properties of the different phases, time of the time step, ascii-file stored information and the model data. +Before beginning the post-processing, it is useful to extract some general information about the model. This includes material properties of the phases, timestep and real time from the timefiles, information of ascii-files and the model data itself. ```julia -using GeophysicalModelGenerator, LaMEM, Serialization +using GeophysicalModelGenerator, LaMEM, JLD2 #### set path and variables -dat_path = ("../test/test_files/Subduction_VEP.dat") -model_path = ("../test/test_files/timestep/") -model_name = "VEP" -timestep = "Timestep_00000000_0.00000000e+00" -FileName_pvtr = "output.pvtr" -p_fields = ["phase", "temperature"] -output_dir = model_path - -# extract data information from ascii file +dat_path = ("../test/test_files/Subduction2D_LaMEM.dat") # path to dat file +model_path = ("../test/test_files/") # path to model timesteps +output_dir =("../test/test_files/output/") # path to output_folder +model_name = "Subduction" # name of the model +timestep = "Timestep_00000000_0.00000000e+00" # name of Timestep +FileName = "output" # name of model output files +FileName_pvtr=FileName*".pvtr" # name of pvtr file +FileName_pvtu=FileName*"_passive_tracers.pvtu" # name of pvtu file +FileName_pvts=FileName*"_surf.pvts" # name of pvts file +p_fields = ["phase", "temperature"] # field to save + + +# extract data information from data file surface_level = search_for_model_constrains(dat_path, "surf_level") # read output file -material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +material_block = Dict{String, Dict{String, Dict{String, String}}}() # dictionary to store material properties material_block = search_for_phase_properties(dat_path, model_name, "", "") +number_phases = length(material_block[model_name]) # extract total number of phases # get the time as a float number +time_file = filter(f -> startswith(f, "Time"), readdir(model_path)) # Extract time information --> timestep and time time = split_at__to_type([timestep],3,"Float") - -# extract data information for selected fields -data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,false) +# extract data information for selected fields, tracer and surface +data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) # model fields +surf = get_surf_timestep(model_path,timestep,FileName_pvts,surface_level) # surface development +tracer = get_tracer_timestep(model_path,timestep,FileName_pvtu) # tracer development ``` -## 2. Start post-processing -To analyse the differences between models and its evolution, coordinates of specific phases can be obtained either as a vector or as a matrix. Additionally, a point can be tracked over time for specific fields. +## 2. Save model information +To analyse the differences between models and its evolution, coordinates of specific phases can be obtained as a matrix. This matrix, together with the corresponding grid configuration and timestep information, provides the basis for extracting field data across all timesteps. Furthermore, surface evolution and tracer distributions can be stored separately for each timestep as welll as the development of a specific grid point. All output is stored in the JLD2 format. +For rapid inspection of model results, snapshots of selected fields can also be generated for each timestep. ```julia @@ -48,14 +56,54 @@ path = replace(processing_folder,"\\" => "/")*"/" indices = get_phase(path,FileName_pvtr,[2],false) matrix = get_phase_bool(path,FileName_pvtr,indices) -# track one point over time -name = "track"*string(indices[1]) -track_point = track_point_over_time(indices[1],p_fields,model_name,model_path,surface_level,name,output_dir,false) +# create a standardized directory structure for organized storage of model outputs +Savefieldfolder = "fields" # folder to store field information +Savegenfolder = "general" # folder to store general information +Savetracerfolder = "tracer" # folder to store tracer information +Savesurffolder = "surf" # folder to store surface information + +# specify plotting attributes +y_slice = [1] # slice in y-direction which should be looked at +dxdz = [-1000.0, 1000.0, -600.0, -50.0] # pLot window, maximum and minimum x and z values in Coordinates --> Float numbers +textpos = [-500.0, -500.0] # position of the text on the field plot +numb_ticks = 6 # number of ticks on axis +phase_to_save = [2,3] # phases to save the fields + +# save general grid properties +find_general_grid_prop(data,time_file,output_dir,Savegenfolder,material_block) + +# save data of fields, surface and tracer for each timestep +for timestep in time_file + @show timestep + data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) # load data from current timestep + post_plot(data,p_fields,timestep,number_phases,y_slice,output_dir, surface_level, dxdz,textpos, numb_ticks;phase_contour=true) # plot fields to see overall development + find_field_properties_grid(data,model_path,timestep,phase_to_save,FileName_pvtr,p_fields,output_dir,Savefieldfolder) # save field properties + find_surf_evolution(model_path,timestep,FileName_pvts,surface_level,output_dir,Savesurffolder) # save surface development + find_tracer_info(model_path,timestep,FileName_pvtu,surface_level,phase_to_save,output_dir,Savetracerfolder) # save tracer development +end + +# track a point over time +Point_coord = CartesianIndex(282, 1, 81) # node coordinate to track over time +track_name = "track_point" # name to save the tracked point + +track_point = track_point_over_time(Point_coord,model_name,Savegenfolder,Savefieldfolder, track_name, output_dir) # track one grid point over time -tracked_point = deserialize_file(output_dir,name) ``` +## 2. Load model information +To use the stored information, load either a specific timestep or the complete temporal evolution across all timesteps. Depending on the analysis, individual timesteps and datasets can be accessed independently, while time-series data can be loaded to examine the development of model properties, phases, tracers, and surface processes throughout the simulation. + +```julia +# load saved information +gen_info = load_field_info(output_dir,Savegenfolder) # load general information +phase_info0 = load_field_info("0",output_dir,Savefieldfolder) # load field info for one specific timestep +phase_info = load_field_info(output_dir,Savefieldfolder) # load field info for all timestep +surf_info = load_field_info(output_dir,Savesurffolder) # load surface information +tracer_info = load_field_info(output_dir,Savetracerfolder) # load tracer information +tracker_info = load_field_info(output_dir,track_name*".jld2") # load tracked point information +``` + -If you want to run the entire example, you can find the .jl code [here](https://github.com/JuliaGeodynamics/GeophysicalModelGenerator.jl/blob/main/tutorial/Tutorial_post_processing.jl) \ No newline at end of file +If you want to run the entire example, you can find the .jl code [here](https://github.com/JuliaGeodynamics/GeophysicalModelGenerator.jl/blob/main/tutorial/Tutorial_post_processing.jl) \ No newline at end of file diff --git a/src/GeophysicalModelGenerator.jl b/src/GeophysicalModelGenerator.jl index 7d000e991..6c36372ae 100644 --- a/src/GeophysicalModelGenerator.jl +++ b/src/GeophysicalModelGenerator.jl @@ -38,7 +38,6 @@ include("Paraview_output.jl") include("Paraview_collection.jl") include("transformation.jl") include("voxel_gravity.jl") -include("LaMEM_io.jl") include("pTatin_IO.jl") include("Setup_geometry.jl") include("stl.jl") diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index bc1b5fab9..e44ea089b 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -2,14 +2,19 @@ using Unitful using NaturalSort using GeophysicalModelGenerator using LaMEM -using Serialization +using JLD2 +using CairoMakie +using Printf +using Statistics +using Plots -export search_for_phase_properties, get_phase_number, search_for_model_constrains, get_phase, -get_phase_bool, get_data_timestep, split_at__to_type, track_point_over_time, deserialize_file - +export search_for_phase_properties, search_for_model_constrains, search_for_all_model_contrains, get_phase, get_phase_bool, split_at__to_type, +get_data_timestep, get_tracer_timestep, get_surf_timestep, +post_plot, find_field_properties_grid, find_general_grid_prop, find_surf_evolution, find_tracer_info, +track_point_over_time """ material_block = search_for_phase_properties(dat_file::String=dat_path, model_name::String = model_name, search_name::String="", stop_name::String="") @@ -46,7 +51,6 @@ function search_for_phase_properties(dat_file::String, model_name::String, searc number_lines = Int[] # To store line numbers where search_name is found stop_lines = Int[] # To store line numbers where stop_name is found - # Open the file and read lines lines = String[] open(dat_file, "r") do file @@ -110,7 +114,7 @@ Examples ```julia -julia> dat_path = ("test/test_files/Subduction_VEP.dat") +julia> dat_path = ("test/input_files/Subduction_VEP.dat") julia> search_name = "surf_level" julia> surface_level = search_for_model_constrains(dat_path, search_name) @@ -120,8 +124,7 @@ julia> surface_level = search_for_model_constrains(dat_path, search_name) ``` """ -# search for values set in the julia file to create and are written to ascii files. - +# search for values set in the julia file to create and are written to ascii files. stops at first found function search_for_model_constrains(path::String, search_name::String) number_lines = Int[] # To store line numbers where search_name is found model_constrain=[] @@ -135,7 +138,8 @@ function search_for_model_constrains(path::String, search_name::String) for i in eachindex(lines) if occursin(search_name, lines[i]) push!(number_lines, i) - con_l = [m.match for m in eachmatch(r"(\d+\.?\d*)", lines[i])] + line = first.(split.(lines[i], '#')) + con_l = [m.match for m in eachmatch(r"(\d+\.?\d*)", line)] for num in con_l push!(model_constrain, parse(Float64, num)) end @@ -146,6 +150,63 @@ function search_for_model_constrains(path::String, search_name::String) return model_constrain end + + +""" + search_value = search_for_all_model_constrains(path::String, search_name::String) + +Parameters +==== +- `path` - path to the ascii file in the model folder +- `search_name` - searching string in the ascii file + + +Examples +======== + +```julia + +julia> dat_path = ("test/input_files/Subduction_VEP.dat") +julia> search_name = "rho" +julia> surface_level = search_for_all_model_constrains(dat_path, search_name) + +1-element Vector{Any}: +3-element Vector{Any}: + 60.0 + 2700.0 + 2750.0 + + ⋮ + 2800.0 + 4000.0 + ``` +""" + +# search for values set in the julia file to create and are written to ascii files. stops at first found +function search_for_all_model_constrains(path::String, search_name::String) + number_lines = Int[] # To store line numbers where search_name is found + model_constrain=[] + + # Open the file and read lines + lines = open(path, "r") do file + readlines(file) # Read all lines at once + end + + # Search for 'search_name' and 'stop_name' in the file + for i in eachindex(lines) + if occursin(search_name, lines[i]) + push!(number_lines, i) + line = first.(split.(lines[i], '#')) + con_l = [m.match for m in eachmatch(r"(\d+\.?\d*)", line)] + for num in con_l + push!(model_constrain, parse(Float64, num)) + end + end + end + + return model_constrain +end + ############################################################################## @@ -183,14 +244,12 @@ julia> phase_coord = get_phase(path,FileName_pvtr,[2],false) # get coordinates of one specific phase - function get_phase(path::String,FileName_pvtr::String,phaseID::Int) indices = [] # processing folder proc_folder = replace(path,"\\" => "/")*"/" - println("Processing folder:" * proc_folder) data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr;fields="phase") @@ -204,36 +263,36 @@ end ########################################################################################################################################### # get Coordinates of the searched phases. Return can either be in one list or separated by phase than as vector{vectors} -function get_phase(path::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ind=false) +function get_phase(location::String,FileName_pvtr::String,phaseIDs::Vector{Int},sep_ind=false) - indices = Int64[] - idx = Int64[] + indices = [] # preallocation + idx = [] # preallocation if length(phaseIDs) == 1 sep_ind=false - ind = get_phase(path,FileName_pvtr,phaseIDs[1]) - indices = ind[1] + ind = get_phase(location,FileName_pvtr,phaseIDs[1]) # get indices of the phase location in matrix + indices = ind[1] else - # Get the indices of the phase in the current row - for i in eachindex(phaseIDs) - - ind = get_phase(path,FileName_pvtr,i) - indices = push!(indices, ind) + for i in eachindex(phaseIDs) # Get indices for multiple phases + ind = get_phase(location,FileName_pvtr,phaseIDs[i]) # get indices of the phase location in matrix + indices = push!(indices, ind) - if sep_ind && i > 1 - idx = vcat(indices[i-1], indices[i]) + # sep ind --> indices for each phase separately listed. Make from Vector{} to Vector{Vector{}} + if (sep_ind == true && i > 1) + idx = vcat(indices[i-1][1], indices[i][1]) end end end - if sep_ind - indices = idx[1] + if sep_ind == true + indices = idx end return indices end + #################################################################################### """ @@ -266,24 +325,19 @@ julia> matrix = get_phase_bool(path,FileName_pvtr,phase_coord) """ # boolse matrix, where coordinates of phase exist values are set to true -function get_phase_bool(path::String,FileName_pvtr::String,ind) +function get_phase_bool(location::String,FileName_pvtr::String,ind) - idx = Int64[] - proc_folder = replace(path,"\\" => "/")*"/" - data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr) - if any(x -> isa(x, Vector), ind) - for i in eachindex(ind) - if i > 1 - idx = vcat(ind[i-1], ind[i]) - end - end - ind = idx[1] - end + proc_folder = replace(location,"\\" => "/")*"/" # correct path for linux + data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr) # read data from path - matrix =zeros(size(data.x.val)) - @views matrix[ind[:]] .= 1 - matrix = Int64.(matrix) + # reduce matrix to Vector{Strings} for the positions + if ind isa Vector{Any} + ind = reduce(vcat, Iterators.flatten(ind)) + end + matrix =zeros(size(data.x.val)) # preallocation + matrix[ind[:]] .= 1 # set all indices which have the correct phase to 1 at the matrix position + matrix = Int64.(matrix) # make the matrix to integer return matrix end @@ -292,7 +346,7 @@ end """ - data = get_data_timestep(model_path::String,timestep::String,FileName_pvtr::String,p_fields::Vector{String},surface_level::Vector{Any},print=false) + data = get_data_timestep(model_path::String,timestep::String,FileName_pvtr::String,p_fields::Vector{String},surface_level::Vector{Any}) Parameters ==== @@ -310,13 +364,13 @@ Examples ```julia julia> model_path = "test/test_files/timestep/" -julia> dat_path = "test/test_files/Subduction_VEP.dat" +julia> dat_path = "test/input_files/Subduction_VEP.dat" julia> timestep = "Timestep_00000000_0.00000000e+00" julia> FileName_pvtr = "output.pvtr" julia> p_fields = ["phase", "temperature"] julia> surface_level = search_for_model_constrains(dat_path, "surf_level") -julia> data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,print=false) +julia> data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) CartData size : (33, 33, 33) @@ -331,13 +385,11 @@ CartData # reads data of one time step and corrects the surface level -function get_data_timestep(model_path::String,timestep::String,FileName_pvtr::String,p_fields::Vector{String},surface_level::Vector{Any},print=false) +function get_data_timestep(model_path::String,timestep::String,FileName_pvtr::String,p_fields::Vector{String},surface_level::Vector{Any}) # processing folder processing_folder = joinpath(model_path,timestep) proc_folder = replace(processing_folder,"\\" => "/")*"/" - print && println("Processing folder:" * proc_folder) - data = read_LaMEM_PVTR_file(proc_folder,FileName_pvtr;fields=p_fields) # Correct surface level @@ -346,6 +398,95 @@ function get_data_timestep(model_path::String,timestep::String,FileName_pvtr::St return data end +############################################################################################### +# get tracer information + +""" + +tracer = get_tracer_timestep(model_path::String,timestep::String,FileName_pvtu::String) + +Parameters +==== +- `model_path` - path to time steps +- `timestep` - time step folder name +- `FileName_pvtu` - file name of the pvtr file +- `print` - if true prints current processing time step + + + +Examples +======== + +```julia + +julia> model_path = "test/test_files/timestep/" +julia> dat_path = "test/input_files/Passive_tracer_ex2D.dat" +julia> timestep = "Timestep_00000000_0.00000000e+00" +julia> FileName_pvtu = "output.pvtu" +julia> p_fields = ["phase", "temperature"] +julia> surface_level = search_for_model_constrains(dat_path, "surf_level") + +julia> tracer = get_tracer_timestep(model_path,timestep,FileName_pvtu) + +""" + +function get_tracer_timestep(model_path::String,timestep::String,FileName_pvtu::String) + + processing_folder = joinpath(model_path,timestep) # path to processing folder + proc_folder = replace(processing_folder,"\\" => "/")*"/" # correct path for linux + + tracer = read_LaMEM_PVTU_file(proc_folder,FileName_pvtu) # read tracer pvtu file + + return tracer +end +############################################################################################################## +# get surface information + + +""" + +surf = get_surf_timestep(model_path::String,timestep::String,FileName_pvts::String,surface_level::Vector{Any}) + +Parameters +==== +- `model_path` - path to time steps +- `timestep` - time step folder name +- `FileName_pvtr` - file name of the pvtr file +- `p_fields` - property fields which should be read +- `surface_level` - get surface level for depth correction +- `print` - if true prints current processing time step + + +Examples +======== + +```julia + +julia> model_path = "test/test_files/timestep/" +julia> dat_path = "test/input_files/Passive_tracer_ex2D.dat" +julia> timestep = "Timestep_00000000_0.00000000e+00" +julia> FileName_pvts = "output.pvts" +julia> p_fields = ["phase", "temperature"] +julia> surface_level = search_for_model_constrains(dat_path, "surf_level") + +julia> surf = get_surf_timestep(model_path,timestep,FileName_pvts,surface_level) + +""" + +function get_surf_timestep(model_path::String,timestep::String,FileName_pvts::String,surface_level::Vector{Any}) + + processing_folder = joinpath(model_path,timestep) # path to processing folder + proc_folder = replace(processing_folder,"\\" => "/")*"/" # correct path for linux + + surf = read_LaMEM_PVTS_file(proc_folder,FileName_pvts) # read surface pvts file + + surf.fields.topography .= surf.fields.topography .- surface_level # Correct surface level + + return surf +end + + + ############################################################################################ @@ -388,141 +529,1062 @@ function split_at__to_type(string_to_split::Vector{String},i::Int64,type::String end +#################################################################################################################### +""" + split_entry = split_at__to_type(string_to_split::String = "Timestep_00000000_0.00000000e+00",i::Int64 = 2,type::String="Float") +Parameters +==== +- `string_to_split` - string which will be split at _ +- `i` - position within the string which will be returned +- `type` - select type how it will be return: Int, Float, or String -#################################################################################################################### +Examples +======== + +```julia +julia> string_to_split = "Timestep_00000000_0.00000000e+00" +julia> split_value = split_at__to_type(string_to_split,2,"Float") + +Float64: + 0.0 + + ``` """ - data = track_point_over_time(Point_coord::CartesianIndex,fields::Vector{String},model_name::String,timefile_location::String,surface_level::Vector, name::String, output_dir::String,save = false) + +function split_at__to_type(timestep::String,i::Int64,type::String) + + # split time string on_ and convert to wished type + if occursin("String", type) + time = split(timestep, '_')[i] + elseif occursin("Float", type) + time = parse(Float64, split(timestep, '_')[i]) + elseif occursin("Int", type) + time = parse(Int64, split(timestep, '_')[i]) + end + return time + +end + + +########################################################################################################################################################################################## +########################################################################################################################################################################################## +########################################################################################################################################################################################## +########################################################################################################################################################################################## +########################################################################################################################################################################################## +########################################################################################################################################################################################## +# save data for postprocessing + + + +""" + + post_plot(data::CartData,p_fields::Vector{String},timestep::String,number_phases::Int64,y_slices::Vector{Int64},output_folder::String,surface_level::Vector,dxdz::Vector{Float64}, textpos::Vector{Float64}, numb_ticks::Int64; phase_contour=false) Parameters ==== -- `Point_coord` - tracked point in CartesianIndex -- `fields` - property fields which should be tracked -- `timefile_location` - path of the time step -- `surface_level` - get surface level for depth correction -- `name` - saving file name -- `output_dir` - save location -- `save` - true: saved, false: not saved +- `data` - CartData structure of the model for one timestep, +- `p_fields` - Fields which should be plotted, +- `timestep` - Timestep name to plot on figure, +- `number_phases` - number of phase to generate the phase colorbar color consistent for all timesteps, +- `y_slices` -select which y slice should be plotted, +- `output_folder` - path to output folder, +- `surface_level` - Elevation of surface in Coordinates, +- `dxdz` - Which part should be plotted in Coordinates, +- `textpos` - position of where the timestep information should be printed on the figure, +- `numb_ticks` -number of ticks on the axis +- `phase_contour` - plot the phase contour on top of the filed, bool) +""" -Examples + +# plot field properties to get first impression of detachment 2D plotting, y slice can be chosen +function post_plot(data::CartData,p_fields::Vector{String},timestep::String,number_phases::Int64,y_slices::Vector{Int64},output_folder::String,surface_level::Vector,dxdz::Vector{Float64}, textpos::Vector{Float64}, numb_ticks::Int64; phase_contour=false) + + # get size of plotting + x_vec = data.x.val[:,1,1] + z_vec = data.z.val[1,1,:] + + xposmin = argmin(abs.(x_vec .- dxdz[1])) + xposmax = argmin(abs.(x_vec .- dxdz[2])) + zposmin = argmin(abs.(z_vec .- (dxdz[3] .+ surface_level[1]))) + zposmax = argmin(abs.(z_vec .- (dxdz[4] .+ surface_level[1]))) + + # get four ticks on each axis + dx = (x_vec[xposmax] - x_vec[xposmin])/(numb_ticks -1) + dz = (z_vec[zposmax] - z_vec[zposmin])/(numb_ticks -1) + + + # determine required decimals + digits_x = max(0, -floor(Int, log10(abs(dx)))) + digits_z = max(0, -floor(Int, log10(abs(dz)))) + + xticks = round.(collect(x_vec[zposmin]:dx:x_vec[xposmax]),digits=digits_x) + yticks = round.(collect(z_vec[zposmin]:dz:z_vec[zposmax]),digits=digits_z) + + + # set figure size + aspect = dx / dz + base = 1200 + + if aspect >= 1 + plot_width = base + plot_height = round(Int, base / aspect) + else + plot_height = base + plot_width = round(Int, base * aspect) + end + + # prevent absurdly small figures + plot_width = max(plot_width, 400) + plot_height = max(plot_height, 300) + cbar_width = 100 + + # general figure properties + size=(plot_width + cbar_width, plot_height) + + xtickfontsize=round(mean(size)*0.025) + ytickfontsize=round(mean(size)*0.025) + labelfontsize=round(mean(size)*0.025) +2 + xlabel="Distance [km]" + ylabel="Depth [km]" + + # title + time = round(parse(Float64, string(split(timestep, "_")[3])),digits=3) + title="t = "* @sprintf("%.5f",time)* " Myr" + titlefontsize =round(maximum(size)*0.03) + + # colorbar + levels=number_phases + colorbar_tickfontsize=round(mean(size)*0.025) + thickness_scaling = 2 + cb_width = round(mean(size)*0.05) + + + for i in eachindex(p_fields) + + # plot parameters reset for each field + field_name = Symbol(p_fields[i]) + colorbar_scale= identity + cb_ticks="automatic" + ticks_pos = WilkinsonTicks(9) + + ############################################################################### + # for matrices in each direction --> 9 directions + if p_fields[i] == "dev_stress" || p_fields[i] == "vel_gr_tensor" + + # get direction + data_direction = ["_xx", "_xy", "_xz", "_yx","_yy","_yz", "_zx", "_zy", "_zz"] + + # load specific fields and set colormap + for j in eachindex(data_direction) + + if "dev_stress" == p_fields[i] + cmap_name="deep" + field_data = getfield(data.fields, field_name)[j] + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cbar_label="Deviatoric stress tensor []" + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "vel_gr_tensor" == p_fields[i] + cmap_name="deep" + field_data = getfield(data.fields, field_name)[j] + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cbar_label="Velocity gradient tensor []" + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + end + + + for s in y_slices + fig = CairoMakie.Figure(size = size) + + ax = Axis(fig[1, 1], xlabel = xlabel, ylabel = ylabel, aspect = DataAspect(), + xticks = xticks, yticks = yticks, xticklabelsize = xtickfontsize, yticklabelsize = ytickfontsize, + xlabelsize = labelfontsize, ylabelsize = labelfontsize) + + hm = CairoMakie.heatmap!(ax, x_vec[xposmin:xposmax], z_vec[zposmin:zposmax], field_data[:, s, :], colormap = cmap, colorscale = colorbar_scale) + + CairoMakie.contour!(ax, x_vec[xposmin:xposmax], z_vec[zposmin:zposmax], data.fields.phase[xposmin:xposmax, s, zposmin:zposmax]; levels = levels, color = :white, linewidth = 0.5,) + CairoMakie.text!(ax,textpos[1], textpos[2]; text = title, align = (:left, :bottom), fontsize = titlefontsize, color = textcolor) + cb = CairoMakie.Colorbar(fig[1, 2], hm;label = cbar_label, width = cb_width, labelsize = colorbar_tickfontsize,ticklabelsize = colorbar_tickfontsize) + cb.height = ax.scene.viewport[].widths[2] + colgap!(fig.layout, 10) + ########################################## + # make directories and save figs + + output_field = joinpath(output_folder,string(p_fields[i],data_direction[j])) + if !isdir(output_field) + mkdir(output_field) + end + + fig_name = "Fig"*string(parse(Int,split(timestep,"_")[2]))*"_"*string(NumValue(data.y[1,s,1]))*".png" + output_name=joinpath(output_field,fig_name) + CairoMakie.save(output_name,fig) + end + end + + + ################################################################################# + # for vector fields not in cell center --> three dircetions + + elseif p_fields[i] == "tot_displ" || p_fields[i] =="velocity" || p_fields[i] =="moment_res" || p_fields[i] == "EHmax" || p_fields[i] == "SHmax" + + # get direction + data_direction = ["_x", "_y", "_z"] + + # load specific fields and set colormap + for j in eachindex(data_direction) + + if "EHmax" == p_fields[i] + cmap_name="deep" + field_data = getfield(data.fields, field_name)[j] + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cbar_label="Maximum horizontal extension []" + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "SHmax" == p_fields[i] + cmap_name="deep" + field_data = getfield(data.fields, field_name)[j] + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cbar_label="Maximum horizontal stress []" + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "tot_displ" == p_fields[i] + cmap_name="deep" + field_data = getfield(data.fields, field_name)[j] + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cbar_label="Total displacements [km]" + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "velocity" == p_fields[i] + cmap_name="speed" + field_data = getfield(data.fields, field_name)[j] + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cbar_label="Velocity [cm/yr]" + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "moment_res" == p_fields[i] + cmap_name="navia" + cbar_label="log₁₀(Momentum residual) [N³]" + field_data = log10.(abs.(getfield(data.fields, field_name)[j])) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + end + + # plot field in specific direction + for s in y_slices + fig = CairoMakie.Figure(size = size) + + ax = Axis(fig[1, 1], xlabel = xlabel, ylabel = ylabel, aspect = DataAspect(), + xticks = xticks, yticks = yticks, xticklabelsize = xtickfontsize, yticklabelsize = ytickfontsize, + xlabelsize = labelfontsize, ylabelsize = labelfontsize) + + + hm = CairoMakie.heatmap!(ax, x_vec[xposmin:xposmax], z_vec[zposmin:zposmax], field_data[:, s, :], colormap = cmap, colorscale = colorbar_scale) + + + CairoMakie.contour!(ax, x_vec[xposmin:xposmax], z_vec[zposmin:zposmax], data.fields.phase[xposmin:xposmax, s, zposmin:zposmax]; levels = levels, color = :white, linewidth = 0.5,) + CairoMakie.text!(ax,textpos[1], textpos[2]; text = title, align = (:left, :bottom), fontsize = titlefontsize, color = textcolor) + cb = CairoMakie.Colorbar(fig[1, 2], hm;label = cbar_label,ticks = ticks_pos, width = cb_width, labelsize = colorbar_tickfontsize,ticklabelsize = colorbar_tickfontsize) + cb.height = ax.scene.viewport[].widths[2] + colgap!(fig.layout, 10) + ########################################## + # make directories and save figs + + output_field = joinpath(output_folder,string(p_fields[i],data_direction[j])) + if !isdir(output_field) + mkdir(output_field) + end + + fig_name = "Fig"*string(parse(Int,split(timestep,"_")[2]))*"_"*string(NumValue(data.y[1,s,1]))*".png" + output_name=joinpath(output_field,fig_name) + CairoMakie.save(output_name,fig) + end + end + else + + #################################################################################### + # for information on cell center --> read specific fields and create colormap + + if "conductivity"== p_fields[i] + cbar_label="Conductivity [W/m/K]" + cmap_name="navia" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true,rev=true) + textcolor = :cyan + + elseif "cont_res" == p_fields[i] + cbar_label="log₁₀(Continuity residual) [1/s]" + cmap_name="navia" + field_data = log10.(abs.(getfield(data.fields, field_name))) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true,rev=true) + textcolor = :cyan + + elseif "density" == p_fields[i] + cbar_label="Density [kg/m³]" + cmap_name="dense" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_val = sort(unique(round.(Int, field_data./ 100) .* 100)) + cmap=cgrad(Symbol(cmap_name), 20, categorical= true, rev=true) + textcolor = :magenta + + elseif "energ_res" == p_fields[i] + cbar_label="log₁₀(Energy residual)" *"[W³]" + cmap_name="navia" + field_data = log10.(abs.(getfield(data.fields, field_name))) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + cmap=cgrad(Symbol(cmap_name),30,categorical=true, rev=true) + cb_ticks = WilkinsonTicks(5) + textcolor = :cyan + + elseif "fluid_density" == p_fields[i] + cbar_label="Fluid density [kg/m³]" + cmap_name="matter" + field_data = getfield(data.fields, field_name)./1e3 # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "j2_dev_stress" == p_fields[i] + cbar_label="Deviatoric stress second invariant " *"[MPa]"#*L"$\tau_{II}$"* + cmap_name="matter" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_val =unique(Int.(round.(unique(filter(!isnan, field_data)),digits=-1))) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "j2_strain_rate" == p_fields[i] + cbar_label="log₁₀(Creep Deviatoric strain rate second invariant) s⁻¹" #L"\dot{\epsilon}" + cmap_name="default" + field_data = log10.(abs.(getfield(data.fields, field_name))) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true) + textcolor = :cyan + + elseif "litho_press" == p_fields[i] + cbar_label="Lithospheric pressure [MPa]"#*L"$^3$"* + cmap_name="matter" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "melt_fraction" == p_fields[i] + cbar_label="Melt fraction []"#*L"$^3$"* + cmap_name="matter" + field_data = getfield(data.fields, field_name)./1e3 # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "over_press" == p_fields[i] + cbar_label="Overpressure [MPa]"#*L"$^3$"* + cmap_name="matter" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "phase" == p_fields[i] + cbar_label="Phase" + cmap_name="plasma" + field_data = getfield(data.fields, field_name)# Access the field using the symbol + field_data = Int.(round.(field_data[xposmin:xposmax,:,zposmin:zposmax])) + field_val = sort(Int.(unique(filter(!isnan, field_data)))) + levels = length(field_val) + col_map=cgrad(Symbol(cmap_name),number_phases,categorical = true) + col = collect(col_map) + col[1] = RGBA(1,1,1,1) + if number_phases > maximum(field_val) +1 + col = col[field_val .+ 1] + end + cmap = PlotUtils.cgrad(col; categorical=true) + cb_values = field_val #.+ 0.5 #collect(0.5:1:levels-0.5) # Custom tick positions + cb_labels = string.(field_val) # Custom labels + ticks_pos = (cb_values ,cb_labels) + textcolor = :cyan + + elseif "plast_dissip" == p_fields[i] + cbar_label="Plastic dissipation [W/m³]" + cmap_name="thermal" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_val = sort(Int.(unique(round.(filter(!isnan, field_data))))) + levels = length(field_val) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true) + textcolor = :cyan + + elseif "plast_strain" == p_fields[i] + cbar_label="log₁₀(Accumulated plastic strain) []" + cmap_name="imola" + field_data = log10.(abs.(getfield(data.fields, field_name))) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + field_val = unique(round.(Int, field_data)) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "pore_press" == p_fields[i] + cbar_label="Pore pressure [MPa]"#*L"$^3$"* + cmap_name="matter" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "pressure" == p_fields[i] + cbar_label="Pressure [MPa]" + cmap_name="matter" # noch durch GPa teilen + field_data = getfield(data.fields, field_name) # is already in MPa + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "rel_dif_rate" == p_fields[i] + cbar_label="Diffusion creep relative strain rate []" + cmap_name="batlow" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :magenta + + elseif "rel_dis_rate" == p_fields[i] + cbar_label="Dislocation creep relative strain rate []" + cmap_name="batlow" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :magenta + + elseif "rel_prl_rate" == p_fields[i] + cbar_label="Relative low-temperature-plasticity creep []" + cmap_name="batlow" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :magenta + + elseif "rel_pl_rate" == p_fields[i] + cbar_label="Peierls creep relative strain rate []" + cmap_name="batlow" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :magenta + + elseif "temperature" == p_fields[i] + cbar_label="Temperature [°C]" + cmap_name="thermal" # noch durch GPa teilen + field_data = getfield(data.fields, field_name) + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_val =unique(Int.(round.(unique(filter(!isnan, field_data)) ./50).*50)) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true) + textcolor = :cyan + + elseif "total_pressure" == p_fields[i] + cbar_label="Total pressure [MPa]" + cmap_name="matter" # noch durch GPa teilen + field_data = getfield(data.fields, field_name) # is already in MPa + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "visc_creep" == p_fields[i] + cbar_label="log₁₀(Creep effective viscosity) [Pa s]" + cmap_name="magma" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + field_val = unique(round.(field_data, digits=1)) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "visc_total" == p_fields[i] + cbar_label="log₁₀(Total effective viscosity) [Pa s]" + cmap_name="magma" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + field_data = ifelse.(isfinite.(field_data), field_data, 0) + field_val = unique(round.(field_data, digits=1)) + cmap=cgrad(Symbol(cmap_name), 30, categorical=true, rev=true) + textcolor = :cyan + + elseif "yield" == p_fields[i] + cbar_label="Yield stress [MPa]" + cmap_name="matter" + field_data = getfield(data.fields, field_name) # Access the field using the symbol + field_data = field_data[xposmin:xposmax,:,zposmin:zposmax] + cmap=cgrad(Symbol(cmap_name), 30, categorical=true) + textcolor = :cyan + + end + + # plot field + for s in y_slices + fig = CairoMakie.Figure(size = size) + + ax = Axis(fig[1, 1], xlabel = xlabel, ylabel = ylabel, aspect = DataAspect(), + xticks = xticks, yticks = yticks, xticklabelsize = xtickfontsize, yticklabelsize = ytickfontsize, + xlabelsize = labelfontsize, ylabelsize = labelfontsize) + + hm = CairoMakie.heatmap!(ax, x_vec[xposmin:xposmax], z_vec[zposmin:zposmax], field_data[:, s, :], colormap = cmap, colorscale = colorbar_scale) + + CairoMakie.contour!(ax, x_vec[xposmin:xposmax], z_vec[zposmin:zposmax], data.fields.phase[xposmin:xposmax, s, zposmin:zposmax]; levels = levels, color = :white, linewidth = 0.5,) + CairoMakie.text!(ax.scene,textpos[1], textpos[2]; text = title, align = (:left, :bottom), fontsize = titlefontsize, color = textcolor) + cb = CairoMakie.Colorbar(fig[1, 2], hm;label = cbar_label,ticks = ticks_pos, width = cb_width, labelsize = colorbar_tickfontsize,ticklabelsize = colorbar_tickfontsize) + cb.height = ax.scene.viewport[].widths[2] + colgap!(fig.layout, 10) + ########################################## + # make directories and save figs + output_field = joinpath(output_folder,p_fields[i]) + if !isdir(output_field) + mkdir(output_field) + end + + fig_name = "Fig"*string(parse(Int,split(timestep,"_")[2]))*"_"*string(NumValue(data.y[1,s,1]))*".png" + output_name=joinpath(output_field,fig_name) + CairoMakie.save(output_name,fig) + end + end + end +end + +######################################################################################################################################################## +# save field properties of specific phases for each timestep + +""" + + find_field_properties_grid(data::CartData,subsubfolder_path::String,timestep::String,phases::Vector{Int64},FileName_pvtr::String,fields::Vector{String},output_path::String,out_folder::String) + + +Parameters +==== +- `data` - CartData structure of the model for one timestep, +- `subsubfolder_path` - path to folder where timesteps are stored +- `timestep` - timestep name to saved, +- `phases` - for which phase should be the information saved, +- `FileName_pvtr` - name of the pvtr file within the timesteps +- `fields` - Fields which should be saved, +- `output_path` - path to where data should stored, +- `out_folder` - name of folder where data gets stored in + +""" + + +function find_field_properties_grid(data::CartData,subsubfolder_path::String,timestep::String,phases::Vector{Int64},FileName_pvtr::String,fields::Vector{String},output_path::String,out_folder::String) + + # get correct path + processing_folder = joinpath(subsubfolder_path,timestep) + location = replace(processing_folder,"\\" => "/")*"/" + + # get phase indices in matrix + indices = get_phase(location,FileName_pvtr,phases,false) + + # create matrix from position of indices + matrix = get_phase_bool(location,FileName_pvtr,indices) + + # write information to dictonary + field_info = Dict() # Dictionary to store material properties + field_info["indices"]= indices + field_info["matrix"]= matrix + + # write field information to dictonary for each direction + for i in eachindex(fields) + + field_n = Symbol(fields[i]) + + if fields[i] == "dev_stress" || fields[i] == "vel_gr_tensor" + + data_direction = ["_xx", "_xy", "_xz", "_yx","_yy","_yz", "_zx", "_zy", "_zz"] + + for j in eachindex(data_direction) + + field_name_dir = string(fields[i]) * string(data_direction[j]) # get field direction name + field_data = getfield(data.fields, field_n)[j] # get field data direction + field_values = field_data[matrix .== 1] # get field of phase + field_info[string(field_name_dir)]= field_values # write to dictonary + end + + + elseif p_fields[i] == "tot_displ" || p_fields[i] =="velocity" || p_fields[i] =="moment_res" || p_fields[i] == "EHmax" || p_fields[i] == "SHmax" + + data_direction = ["_x", "_y", "_z"] + + for j in eachindex(data_direction) + + field_name_dir = string(fields[i]) * string(data_direction[j])# get field direction name + field_data = getfield(data.fields, field_n)[j] # get field data direction + field_values = field_data[matrix .== 1] # get field of phase + field_info[string(field_name_dir)]= field_values # write to dictonary + + end + + else + + field_data = getfield(data.fields, field_n)# get field data direction + field_values = field_data[matrix .== 1] # get field of phase + field_info[string(fields[i])]= field_values # write to dictonary + end + + end + + # create output directory if not existing + output_path = joinpath(output_path,out_folder) + if !isdir(output_path) + mkdir(output_path) + end + + # create output name and path and save it + file_name = out_folder*string(parse(Int,split(split(location, "Timestep_")[2],"_")[1]))*".jld2" + output_name=joinpath(output_path,file_name) + + # jld2 files + JLD2.jldsave(output_name; field_info) + + return +end + +##################################################################################################################################### +# saves genreall information about the grid and timesteps + + +""" + + find_general_grid_prop(data::CartData,time_file::Vector{String},output_path::String,out_folder::String,material_block::Dict) + + +Parameters +==== +- `data` - CartData structure of the model for one timestep, +- `time_file` - timestep where general information gets extracted +- `output_path` - path to where data should stored, +- `out_folder` - name of folder where data gets stored in, +- `material_block` - Dict where material properties for each phase is stored , + +""" + + +function find_general_grid_prop(data::CartData,time_file::Vector{String},output_path::String,out_folder::String,material_block::Dict) + + field_info = Dict() # dictionary to store material properties + field_info["x"]= data.x.val # store x coorindates + field_info["y"]= data.y.val # store y coorindates + field_info["z"]= data.z.val # store z coorindates + field_info["phase_init"] = data.fields.phase # store Initial phase setup + field_info["timesteps"] = time_file # save timesteps + field_info["material_block"] = material_block # Save material properties of all phases + + # create output path + output_path = joinpath(output_path,out_folder) + if !isdir(output_path) + mkdir(output_path) + end + + file_name = out_folder *".jld2" + output_name=joinpath(output_path,file_name) + + # jld2 files + JLD2.jldsave(output_name; field_info) + + return +end + + +##################################################################################################################################### +# saves surface development for each timestep + +""" + + find_surf_evolution(subsubfolder_path::String,timestep::String,FileName_pvts::String,surface_level::Vector{Any},output_path::String,out_folder::String) + + +Parameters +==== +- `subsubfolder_path` - Path to folder where timesteps are stored +- `timestep` - Timestep name to saved, +- `FileName_pvts` - name of the pvtr file within the timesteps +- `surface_level` - surface level in coordinates, +- `output_path` - path to where data should stored, +- `out_folder` - name of folder where data gets stored in + +""" + + +function find_surf_evolution(subsubfolder_path::String,timestep::String,FileName_pvts::String,surface_level::Vector{Any},output_path::String,out_folder::String) + + # read surface information + surf = get_surf_timestep(subsubfolder_path,timestep,FileName_pvts,surface_level) + + surf_info = Dict() # Dictionary to store material properties + surf_info["velocity"]= surf.fields.velocity # Store surface velocity + surf_info["topography"]= surf.fields.topography # Store surface topography + + # create output directory + output_path = joinpath(output_path,out_folder) + if !isdir(output_path) + mkdir(output_path) + end + + file_name = out_folder*string(parse(Int,split(split(timestep, "Timestep_")[2],"_")[1]))*".jld2" + output_name=joinpath(output_path,file_name) + + # jld2 files + JLD2.jldsave(output_name; surf_info) + + return +end + +###################################################################################################################################################### +# saves tracer development for each timestep + + +""" + + find_tracer_info(subsubfolder_path::String,timestep::String,FileName_pvtu::String,surface_level::Vector{Any},phase_numb::Vector{Int},output_path::String,out_folder::String) + + +Parameters +==== +- `subsubfolder_path` - Path to folder where timesteps are stored +- `timestep` - Timestep name to saved, +- `FileName_pvtu` - name of the pvtr file within the timesteps +- `surface_level` - surface level in coordinates, +- `phase_numb` - extract tracers only from specified phases, +- `output_path` - path to where data should stored, +- `out_folder` - name of folder where data gets stored in + +""" + + + +function find_tracer_info(subsubfolder_path::String,timestep::String,FileName_pvtu::String,surface_level::Vector{Any},phase_numb::Vector{Int},output_path::String,out_folder::String) + + tracer = get_tracer_timestep(subsubfolder_path,timestep,FileName_pvtu) # read all tracer + tracer_in_slab = findall(x -> x in phase_numb, tracer.fields.Phase) # read all tracer in phase + tracer_indices = tracer_in_slab .- 1 # correct position numbers to tracer numbers (start zero and one) + + tracer_info = Dict() # Dictionary to store material properties + tracer_info["x"]= try tracer.x.val[tracer_indices] catch; nothing end # save x position + tracer_info["y"]= try tracer.y.val[tracer_indices] catch; nothing end # save y position + tracer_info["z"]= try tracer.z.val[tracer_indices] .- surface_level catch; nothing end # save z position + tracer_info["Phase"]= try tracer.fields.Phase[tracer_indices] catch; nothing end # save phase + tracer_info["Temperature"]= try tracer.fields.Temperature[tracer_indices] catch; nothing end # save temperature + tracer_info["Pressure"]= try tracer.fields.Pressure[tracer_indices] catch; nothing end # save pressure + tracer_info["ID"]= try tracer.fields.ID[tracer_indices] catch; nothing end # save ID of tracer + tracer_info["MF"]= try tracer.fields.Mf[tracer_indices] catch; nothing end # save melt fraction of tracer + tracer_info["MF_Grid"]= try tracer.fields.Mf_Grid[tracer_indices] catch; nothing end # save grid of melt fraction of tracer + tracer_info["Active"]= try tracer.fields.Active[tracer_indices] catch; nothing end # save state + + + # create output directory + output_folder = joinpath(output_path,out_folder) + if !isdir(output_folder) + mkdir(output_folder) + end + + + filename = out_folder * string(split_at__to_type([timestep],2,"Int64")[1])*".jld2" + output_name=joinpath(output_folder,filename) + + #jld2 files + JLD2.jldsave(output_name; tracer_info) + + +end + + +""" + +Examples ======== -```julia + ```julia + + +julia> FileName = "output" +julia> FileName_pvtr=FileName*".pvtr" # Name of pvtr file change if pvtr has a different name +julia> FileName_pvtu=FileName*".pvtu" # Name of pvtr file change if pvtu has a different name +julia> FileName_pvts=FileName*".pvts" # Name of pvtr file change if pvts has a different name + +julia> folder = "test" # folder where timesteps are saved in +julia> current_folder = "test" # location of where FileName folder is stored +julia> model_path = "test/input_files/timestep/" +julia> dat_path = "test/input_files/Passive_tracer_ex2D.dat" +julia> output_dir =joinpath(current_folder, "output") + +julia> if !isdir(output_dir) +julia> mkdir(output_dir) +julia> end + +julia> p_fields = ["phase","temperature"] # field which you saved +julia> y_slice = [15] # which y slice should be looked at, best is in the middle + +julia> dxdz = [0.3, 0.8, 0.2, 0.7] # Maximum and minimum x and z values in Coordinates --> Float numbers +julia> phase_to_save = [2,3] # phases to save the location an properties +julia> numb_ticks = 6 # phases to save the location an properties +julia> Savefilefolder = "fields" # folder where field information are stored +julia> Savegenfolder = "general" # folder where general information are stored +julia> Savetracerfolder = "tracer" # folder where tracer information are stored +julia> Savesurffolder = "surf" # folder where surface information are stored + +################################################################################################################################################################################ +################################################################################################################################################################################ -julia> Point_coord = CartesianIndex(10, 10, 10) -julia> model_name = "PEV" -julia> timefile_location = "test/test_files/timestep/" -julia> timestep = "Timestep_00000000_0.00000000e+00" -julia> p_fields = ["phase", "temperature"] julia> surface_level = search_for_model_constrains(dat_path, "surf_level") -julia> name = "track"*string(CartesianIndex(200,1,200)) -julia> output_dir = "test/test_files/timestep/" -julia> track_point = track_point_over_time(Point_coord,p_fields,model_name,timefile_location,surface_level, name::String, output_dir::String,save = false) +julia> material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +julia> material_block = search_for_phase_properties(dat_path, folder, "", "") +julia> number_phases = length(material_block[folder]) # extract total number of phases -Dict{String, Dict{String, Float64}} with 2 entries: - "Timestep_00000010_1.49079279e+01" => Dict("phase"=>1.53445, "temperature"=>0.0) - "Timestep_00000000_0.00000000e+00" => Dict("phase"=>2.0, "temperature"=>0.0) - ... +julia> time_file = filter(f -> startswith(f, "Time"), readdir(model_path)) # Extract time information --> timestep and time + +################################################################################################################################################################################ +################################################################################################################################################################################ + +julia> for timestep in time_file # go through all timesteps of a model + + data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) # read model information + + post_plot(data,p_fields,timestep,number_phases,y_slice,output_dir, surface_level, dxdz,textpos, numb_ticks;phase_contour=true) # plot fields to see overall development + + if timestep == "Timestep_00000000_0.00000000e+00" + find_general_grid_prop(data,timestep,output_dir,Savegenfolder,material_block) # get general information from first time step + end + + find_field_properties_grid(data,model_path,timestep,phase_to_save,FileName_pvtr,p_fields,output_dir,Savefilefolder) #save field properties + + #find_surf_evolution(model_path,timestep,FileName_pvts,surface_level,output_dir,Savesurffolder) # save surface development + + #find_tracer_info(model_path,timestep,FileName_pvtu,surface_level,phase_to_save,output_dir,Savetracerfolder) # save tracer development + +julia> end ``` + """ +####################################################################################################################################################### +####################################################################################################################################################### +####################################################################################################################################################### +####################################################################################################################################################### +####################################################################################################################################################### +####################################################################################################################################################### +####################################################################################################################################################### +####################################################################################################################################################### -# track one specific point over time for multiple fields +""" -function track_point_over_time(Point_coord::CartesianIndex,fields::Vector{String},model_name::String,timefile_location::String,surface_level::Vector, name::String, output_dir::String,save = false) + field_info = load_field_info(timestep::String,output_path::String,output_folder::String) - track_point = Dict{String,Dict{String,Float64}}() +Parameters +============== - time_files = filter(f -> startswith(f, "Time"), readdir(timefile_location)) - save_dict = Dict{String,Dict{String,Dict{String,Float64}}}() +- `timestep` - Timestep Integer number to loaded, +- `output_path` - Path to folder where files are stored in, +- `output_folder` - name of files - for timestep in time_files +""" - data = get_data_timestep(timefile_location,timestep,FileName_pvtr,fields,surface_level,false) - track_point[timestep] = Dict{String,Float64}() +# Extract file information again --> FROM saved files - for field in fields +# load saved field data for one specific time step +function load_field_info(timestep::String,output_path::String,output_folder::String) + + # load saved field informations + output_slab = joinpath(output_path,output_folder) + output_det = sort(readdir(output_slab), lt=natural) # sort files in output folder according to thte time steps - field_name = Symbol(field) - field_data = getfield(data.fields, field_name) + file_name = filter(s -> occursin(timestep,s), output_det)[1] # read specific times step file - track_point[timestep][field] = field_data[Point_coord.I[1],Point_coord.I[2],Point_coord.I[3]] + output_file = joinpath(output_slab,file_name) # path to correct output file which should be read + # preallocation + phase_info = [] + + jldopen(joinpath(output_file), "r") do f + push!(phase_info,f[keys(f)[1]]) end - end + return phase_info[1] +end + +############################################################################################################################ +# load saved field data for all timesteps + + +""" + + + field_info = load_field_info(output_path::String,output_folder::String) + +Parameters +============== + +- `output_path` - Path to folder where files are stored in, +- `output_folder` - Mame of files, loads all files in the folder with this name - if save - save_dict[model_name] = Dict{String,Dict{String,Float64}}() - save_dict[model_name] = track_point - file_name = string(name)*".txt" - output_name=joinpath(output_dir,file_name) - # Serialize the vector of structures to a file - open(output_name, "a") do file - serialize(file, save_dict) +Example +=============== + +julia> Savegenfolder = "general" # folder where general information are stored + +julia> current_folder = "test" # location of where FileName folder is stored +julia> output_dir =joinpath(current_folder, "output") + +julia> gen_info = load_field_info(output_dir,Savegenfolder)[1] + +Dict{Any, Any} with 6 entries: + "x" => [0.0 0.0 … 0.0 0.0; 0.03125 0.03125 … 0.03125 0.03125; … ; 0.96875 0.96875 … 0.96875 0.96875; 1.0 1.0 … 1.0 1.0;;; 0.0 0.0 … 0.0 0.0; 0.03125 0.03125 … 0.03125 0.03125; … ; 0.96875 0.96875 …… + "phase_init" => Float32[0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0;;; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0;;; 0.0 0.0 … 0.0 0.0;… + "timesteps" => "Timestep_00000000_0.00000000e+00" + "z" => [0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0;;; 0.03125 0.03125 … 0.03125 0.03125; 0.03125 0.03125 … 0.03125 0.03125; … ; 0.03125 0.03125 … 0.03125 0.03125… + "material_block" => Dict("test"=>Dict("Phase5"=>Dict("Cp"=>"1.2e3 # heat capacity", "ID"=>"5", "fr"=>"30 # friction angle [deg]", "k"=>"2.5", "alpha"=>"1e-5", "disl_prof"=>"Dry_Olivine_disl_creep-Hirth_Kohls… + "y" => [0.0 0.03125 … 0.96875 1.0; 0.0 0.03125 … 0.96875 1.0; … ; 0.0 0.03125 … 0.96875 1.0; 0.0 0.03125 … 0.96875 1.0;;; 0.0 0.03125 … 0.96875 1.0; 0.0 0.03125 … 0.96875 1.0; … ; 0.0 0.03125 … 0.9… + + +""" + + +function load_field_info(output_path::String,output_folder::String) + + output_slab = joinpath(output_path,output_folder) # get folder location + phase_info = [] # preallocation + + if isdir(output_slab) + output_det = sort(readdir(output_slab), lt=natural) # get all in folder + + # read each file in the folder and write it to the vector + for t in eachindex(output_det) + output_file = joinpath(output_slab,output_det[t]) + + jldopen(joinpath(output_file), "r") do f + push!(phase_info,f[keys(f)[1]]) + end end - end + elseif isfile(output_slab) + # read each file in the folder and write it to the vector + + jldopen(joinpath(output_slab), "r") do f + push!(phase_info,f[keys(f)[1]]) + end - return track_point + end + return phase_info end -################################################################################################################### + + + +#################################################################################################################### """ - file = deserialize_file(output_dir::String,name::String) + data = track_point_over_time(Point_coord::CartesianIndex,folder::String,Savegenfolder::String,Savefieldfolder::String, name::String, output_path::String) + Parameters ==== -- `output_dir` - directory of the file -- `name` - name of the file +- `Point_coord` - tracked point in CartesianIndex +- `folder` - name of model +- `Savegenfolder` - name of folder where general informations are stored +- `Savefieldfolder` - name of folder where field informations are stored +- `surface_level` - get surface level for depth correction +- `name` - file name to save information +- `output_path` - location where the output of the model is stored -Examples +Examples ======== ```julia -julia> output_dir = "./output/ -julia> name = "track"*string(CartesianIndex(10,10,10)) -julia> file_info = deserialize_file(output_dir,name) +julia> Point_coord = CartesianIndex(10, 10, 10) +julia> folder = "PEV" +julia> Savegenfolder = "general" +julia> Savefieldfolder = "field" +julia> name = "track_output" +julia> output_path = "test/input_files/output/" -1-element Vector{Any}: - Dict("VEP" => Dict("Timestep_00000010_1.49079279e+01" => Dict("phase" => 1.534447431564331, "temperature" => 0.0), - "Timestep_00000000_0.00000000e+00" => Dict("phase" => 2.0, "temperature" => 0.0))) +julia> track_point = track_point_over_time(Point_coord::CartesianIndex,folder::String,Savegenfolder::String,Savefieldfolder::String, name::String, output_path::String) +Dict{String, Dict{String, Float64}} with 2 entries: + "Timestep_00000010_1.49079279e+01" => Dict("phase"=>1.53445, "temperature"=>0.0) + "Timestep_00000000_0.00000000e+00" => Dict("phase"=>2.0, "temperature"=>0.0) + ... ``` """ -# load and extract information of serialized files +# track one specific point over time for multiple fields -function deserialize_file(output_dir::String,name::String) +function track_point_over_time(Point_coord::CartesianIndex,folder::String,Savegenfolder::String,Savefieldfolder::String, name::String, output_path::String) - file_name = name * ".txt" - output_name=joinpath(output_dir,file_name) + # read saved dictonaries for phases, generall info, timesteps, and indices and saved fields + phase_info = load_field_info(output_path,Savefieldfolder) + gen_info = load_field_info(output_path,Savegenfolder)[1] + time_files = gen_info["timesteps"] + fields = collect(keys(phase_info[1])) + fields = filter(x -> x != "indices", fields) - det_info =[] + #get index of vector which contains only slab information + ind = reduce(vcat, Iterators.flatten(phase_info[1]["indices"])) + index = findfirst(x -> x == Point_coord, ind)[1] + + # create empty dictory to save the time dependent locations of points + track_point = Dict{String,Dict{String,Float64}}() + save_dict = Dict{String,Dict{String,Dict{String,Float64}}}() + + for t in eachindex(time_files) + + track_point[time_files[t]] = Dict{String,Float64}() # add timestep information to Dictonary + + for field in fields + + + field_value = phase_info[t][field][index] # extract field information for indices at specific time step + track_point[time_files[t]][field] = field_value# add field and index information to Dictonary - open(output_name, "r") do file - while !eof(file) - loaded_detach_instances = deserialize(file) - push!(det_info, loaded_detach_instances) end end - return det_info + # add model name to dictonary to save all models in one + save_dict[folder] = Dict{String,Dict{String,Float64}}() + save_dict[folder] = track_point + + # create output information + file_name = string(name)*".jld2" + output_name=joinpath(output_dir,file_name) + + jldopen(output_name, "w") do f + f["save_dict"] = save_dict + end + + return save_dict + end diff --git a/src/Setup_geometry.jl b/src/Setup_geometry.jl index 64d736f07..c6fcd04cd 100644 --- a/src/Setup_geometry.jl +++ b/src/Setup_geometry.jl @@ -664,10 +664,12 @@ for i = 1:size(Y)[2] end end +Zrel = Z .- maximum(zlim); +println("add poly") # Compute thermal structure accordingly. See routines below for different options if T != nothing - Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T) + Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Zrel[ind], Phase[ind], T) end # Set the phase. Different routines are available for that - see below. diff --git a/test/runtests.jl b/test/runtests.jl index 39a947fb6..8125c32f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -30,10 +30,6 @@ using Test include("test_surfaces.jl") end - @testset "LaMEM" begin - include("test_lamem.jl") - end - @testset "pTatin" begin include("test_pTatin_IO.jl") end @@ -81,7 +77,7 @@ using Test include("test_ASAGI_IO.jl") end - @testset "LaMEM_post_processing" begin + @testset "LaMEM_post" begin include("test_LaMEM_post_processing.jl") end diff --git a/test/test_LaMEM_post_processing.jl b/test/test_LaMEM_post_processing.jl index bed11ef55..7e4d00400 100644 --- a/test/test_LaMEM_post_processing.jl +++ b/test/test_LaMEM_post_processing.jl @@ -1,30 +1,43 @@ -using GeophysicalModelGenerator, LaMEM, Serialization, Test - +using GeophysicalModelGenerator, JLD2, Test, CairoMakie, Printf, Statistics # load data file -dat_path = ("./test_files/Subduction_VEP.dat") -model_path = ("./test_files/timestep/") -model_name = "VEP" -timestep = "Timestep_00000000_0.00000000e+00" -FileName_pvtr = "output.pvtr" -p_fields = ["phase", "temperature"] -output_dir = model_path - -# test extraction from ascii files +dat_path = ("../test/test_files/Subduction2D_LaMEM.dat") # path to dat file +model_path = ("../test/test_files/") # path to model timesteps +output_dir =("../test/test_files/output/") # path to output_folder +model_name = "Subduction" # name of the model +timestep = "Timestep_00000000_0.00000000e+00" # name of Timestep +FileName = "output" # name of model output files +FileName_pvtr=FileName*".pvtr" # name of pvtr file +FileName_pvtu=FileName*"_passive_tracers.pvtu" # name of pvtu file +FileName_pvts=FileName*"_surf.pvts" # name of pvts file +p_fields = ["phase", "temperature"] # field to save +Savefieldfolder = "fields" # folder to store field information +Savegenfolder = "general" # folder to store general information +Savetracerfolder = "tracer" # folder to store tracer information +Savesurffolder = "surf" # folder to store surface information +y_slice = [1] # slice in y-direction which should be looked at +dxdz = [-1000.0, 1000.0, -600.0, -50.0] # pLot window, maximum and minimum x and z values in Coordinates --> Float numbers +textpos = [-500.0, -500.0] # position of the text on the field plot +numb_ticks = 6 # number of ticks on axis +phase_to_save = [2,3] # phases to save the fields +Point_coord = CartesianIndex(282, 1, 81) # node coordinate to track over time +name = "track_point" # name to save the tracked point + # extract data information from data file surface_level = search_for_model_constrains(dat_path, "surf_level") gravity = search_for_model_constrains(dat_path, "gravity") +rhos = search_for_all_model_constrains(dat_path, "rho") -@test surface_level == [0.0] -@test gravity == [0.0, 0.0, 10.0] +@test gravity == [0.0, 0.0, 9.81] +@test rhos == [3300.0, 3300.0, 3300.0, 2700.0, 3300.0, 50.0] +@test surface_level == Any[0.0] # read output file -material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +material_block = Dict{String, Dict{String, Dict{String, String}}}() # dictionary to store material properties material_block = search_for_phase_properties(dat_path, model_name, "", "") -@test material_block[model_name]["Phase5"]["fr"] == "30 # friction angle [deg]" -@test material_block[model_name]["Phase0"]["rho"] == "100" - +@test material_block[model_name]["Phase1"]["ID"] == "1 # Material phase ID" +@test material_block[model_name]["Phase0"]["rho"] =="3300.0 # Density [kg/m^3]" # test time extraction # get the time as a float number @@ -38,27 +51,61 @@ processing_folder = joinpath(model_path,timestep) path = replace(processing_folder,"\\" => "/")*"/" indices = get_phase(path,FileName_pvtr,[2],false) matrix = get_phase_bool(path,FileName_pvtr,indices) -@test sum(matrix) == 343 -@test length(indices) == 343 +@test sum(matrix) == 9406 +@test length(indices) == 9406 @test matrix[indices[1]] == 1 @test matrix[CartesianIndex(1,1,1)] == 0 # extract data information -pvtr_path = joinpath(model_path,timestep)*"/" -data_pvtr = read_LaMEM_PVTR_file(pvtr_path,FileName_pvtr;fields = p_fields) -data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,false) +pvt_path = joinpath(model_path,timestep)*"/" +data_pvtr = read_LaMEM_PVTR_file(pvt_path,FileName_pvtr;fields = p_fields) +surf_pvts = read_LaMEM_PVTS_file(pvt_path,FileName_pvts) +tracer_pvtu = read_LaMEM_PVTU_file(pvt_path,FileName_pvtu) +data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) +surf = get_surf_timestep(model_path,timestep,FileName_pvts,surface_level) +tracer = get_tracer_timestep(model_path,timestep,FileName_pvtu) @test (getindex(data.z[1,1,1])) == (getindex(data_pvtr.z[1,1,1])) +@test (getindex(surf.z[1,1,1])) == (getindex(surf_pvts.z[1,1,1])) +@test (getindex(tracer.z[1,1,1])) == (getindex(tracer_pvtu.z[1,1,1])) + + +number_phases = length(material_block[model_name]) # extract total number of phases +time_file = filter(f -> startswith(f, "Time"), readdir(model_path)) # extract time information --> timestep and time + +post_plot(data,p_fields,time_file[1],number_phases,y_slice,output_dir, surface_level, dxdz,textpos, numb_ticks;phase_contour=true) # plot fields to see overall development + +find_general_grid_prop(data,time_file,output_dir,Savegenfolder,material_block) # get general information from first time step + +find_field_properties_grid(data,model_path,time_file[1],phase_to_save,FileName_pvtr,p_fields,output_dir,Savefieldfolder) #save field properties +find_surf_evolution(model_path,time_file[1],FileName_pvts,surface_level,output_dir,Savesurffolder) # save surface development +find_tracer_info(model_path,time_file[1],FileName_pvtu,surface_level,phase_to_save,output_dir,Savetracerfolder) # save tracer development + +# load saved information +gen_info = load_field_info(output_dir,Savegenfolder)[1] +phase_info0 = load_field_info("0",output_dir,Savefieldfolder) +phase_info = load_field_info(output_dir,Savefieldfolder) +surf_info = load_field_info(output_dir,Savesurffolder) +tracer_info = load_field_info(output_dir,Savetracerfolder)[1] + +@test collect(keys(gen_info)) == Any["x", "phase_init", "timesteps", "z", "material_block", "y"] +@test unique(Int.(round.(gen_info["phase_init"]))) == [0 , 1, 2, 3, 4, 5] + +@test isfile(joinpath(output_dir,p_fields[1], "Fig0_-2.5.png")) + +@test typeof(phase_info0) == Dict{Any,Any} +@test typeof(phase_info) == Vector{Any} +@test typeof(surf_info) == Vector{Any} +@test typeof(tracer_info) == Dict{Any, Any} + +#track one point over time +track_point = track_point_over_time(Point_coord,model_name,Savegenfolder,Savefieldfolder, name, output_dir) +tracker_info = load_field_info(output_dir,"track_point.jld2")[1] -# track one point over time -name = "track"*string(indices[1]) -track_point = track_point_over_time(indices[1],p_fields,model_name,model_path,surface_level,name,output_dir,false) +@test first(keys(tracker_info[model_name])) == "Timestep_00000000_0.00000000e+00" +@test track_point[model_name]["Timestep_00000000_0.00000000e+00"]["phase"] == 2.0 -@test track_point["Timestep_00000010_1.49079279e+01"]["phase"] == 1.534447431564331 -@test track_point["Timestep_00000000_0.00000000e+00"]["phase"] == 2.0 -tracked_point = deserialize_file(output_dir,name) -@test first(keys(track_point)) == "Timestep_00000010_1.49079279e+01" \ No newline at end of file diff --git a/test/test_files/Timestep_00000000_0.00000000e+00/output.pvtr b/test/test_files/Timestep_00000000_0.00000000e+00/output.pvtr new file mode 100644 index 000000000..b4fdcef1e --- /dev/null +++ b/test/test_files/Timestep_00000000_0.00000000e+00/output.pvtr @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_files/Timestep_00000000_0.00000000e+00/output_passive_tracers.pvtu b/test/test_files/Timestep_00000000_0.00000000e+00/output_passive_tracers.pvtu new file mode 100644 index 000000000..1032679d3 --- /dev/null +++ b/test/test_files/Timestep_00000000_0.00000000e+00/output_passive_tracers.pvtu @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_files/Timestep_00000000_0.00000000e+00/output_surf.pvts b/test/test_files/Timestep_00000000_0.00000000e+00/output_surf.pvts new file mode 100644 index 000000000..88f08bc26 --- /dev/null +++ b/test/test_files/Timestep_00000000_0.00000000e+00/output_surf.pvts @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/test/test_files/output/phase/Fig0_-2.5.png b/test/test_files/output/phase/Fig0_-2.5.png new file mode 100644 index 0000000000000000000000000000000000000000..53819015984ddd047c820c62689c8272daf8860a GIT binary patch literal 82173 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 D!#V&4 literal 0 HcmV?d00001 diff --git a/test/test_files/output/temperature/Fig0_-2.5.png b/test/test_files/output/temperature/Fig0_-2.5.png new file mode 100644 index 0000000000000000000000000000000000000000..7a5e8887ff1e47361e22bb483a483378e750a04e GIT binary patch literal 101819 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%0DbNhQsA*wGxwif4P%1`Z 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®?6R);Vb$Sn%Zil1|gUMQ>It?7Qf&qqHM_Yv20P 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 Float numbers +textpos = [-500.0, -500.0] # position of the text on the field plot +numb_ticks = 6 # number of ticks on axis +phase_to_save = [2,3] # phases to save the fields +Point_coord = CartesianIndex(282, 1, 81) # node coordinate to track over time +track_name = "track_point" # name to save the tracked point + # extract data information from data file surface_level = search_for_model_constrains(dat_path, "surf_level") # read output file -material_block = Dict{String, Dict{String, Dict{String, String}}}() # Dictionary to store material properties +material_block = Dict{String, Dict{String, Dict{String, String}}}() # dictionary to store material properties material_block = search_for_phase_properties(dat_path, model_name, "", "") +number_phases = length(material_block[model_name]) # extract total number of phases # get the time as a float number +time_file = filter(f -> startswith(f, "Time"), readdir(model_path)) # Extract time information --> timestep and time time = split_at__to_type([timestep],3,"Float") -# extract data information -data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level,false) +# extract data information from models +data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) # model fields +surf = get_surf_timestep(model_path,timestep,FileName_pvts,surface_level) # surface development +tracer = get_tracer_timestep(model_path,timestep,FileName_pvtu) # tracer development -# get information about where the phase is located +# get information about where specific phases are located processing_folder = joinpath(model_path,timestep) path = replace(processing_folder,"\\" => "/")*"/" indices = get_phase(path,FileName_pvtr,[2],false) matrix = get_phase_bool(path,FileName_pvtr,indices) -# track one point over time -name = "track"*string(indices[1]) -track_point = track_point_over_time(indices[1],p_fields,model_name,model_path,surface_level,name,output_dir,false) +# save general grid properties +find_general_grid_prop(data,time_file,output_dir,Savegenfolder,material_block) + +# save data of fields, surface and tracer for each timestep +for timestep in time_file + @show timestep + data = get_data_timestep(model_path,timestep,FileName_pvtr,p_fields,surface_level) # load data from current timestep + post_plot(data,p_fields,timestep,number_phases,y_slice,output_dir, surface_level, dxdz,textpos, numb_ticks;phase_contour=true) # plot fields to see overall development + find_field_properties_grid(data,model_path,timestep,phase_to_save,FileName_pvtr,p_fields,output_dir,Savefieldfolder) # save field properties + find_surf_evolution(model_path,timestep,FileName_pvts,surface_level,output_dir,Savesurffolder) # save surface development + find_tracer_info(model_path,timestep,FileName_pvtu,surface_level,phase_to_save,output_dir,Savetracerfolder) # save tracer development +end -tracked_point = deserialize_file(output_dir,name) +# track a point over time +track_point = track_point_over_time(Point_coord,model_name,Savegenfolder,Savefieldfolder, track_name, output_dir) # track one grid point over time +# load saved information +gen_info = load_field_info(output_dir,Savegenfolder) # load general information +phase_info0 = load_field_info("0",output_dir,Savefieldfolder) # load field info for one specific timestep +phase_info = load_field_info(output_dir,Savefieldfolder) # load field info for all timestep +surf_info = load_field_info(output_dir,Savesurffolder) # load surface information +tracer_info = load_field_info(output_dir,Savetracerfolder) # load tracer information +tracker_info = load_field_info(output_dir,track_name*".jld2") # load tracked point information From c9fd36db31b72f72bc4d234b586670bbae4e2df5 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:17:37 +0200 Subject: [PATCH 15/16] Resolved conflicts --- docs/src/man/lamem.md | 20 - .../src/man/tutorial_lamem_post_processing.md | 2 +- src/LaMEM_post_processing.jl | 14 +- src/Setup_geometry.jl | 1802 +++++++++++------ test/runtests.jl | 102 +- .../ProcessorPartitioning_4cpu_1.2.2.bin | Bin 856 -> 0 bytes test/test_files/Subduction_VEP.dat | 273 --- .../output.pvtr | 25 - .../output_phase.pvtr | 16 - .../output.pvtr | 25 - .../output_phase.pvtr | 16 - .../trackCartesianIndex(10, 10, 10).txt | Bin 232 -> 0 bytes test/test_lamem.jl | 193 ++ 13 files changed, 1376 insertions(+), 1112 deletions(-) delete mode 100644 docs/src/man/lamem.md delete mode 100644 test/test_files/ProcessorPartitioning_4cpu_1.2.2.bin delete mode 100644 test/test_files/Subduction_VEP.dat delete mode 100644 test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr delete mode 100644 test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr delete mode 100644 test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr delete mode 100644 test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr delete mode 100644 test/test_files/timestep/trackCartesianIndex(10, 10, 10).txt create mode 100644 test/test_lamem.jl diff --git a/docs/src/man/lamem.md b/docs/src/man/lamem.md deleted file mode 100644 index 7f081051c..000000000 --- a/docs/src/man/lamem.md +++ /dev/null @@ -1,20 +0,0 @@ -# LaMEM - -In order to generate geodynamic simulations from setups created with `GeophysicalModelGenerator.jl`, we provide a few routines that directly create marker input files for the 3D geodynamic modelling software [LaMEM](https://github.com/UniMainzGeo/LaMEM), which is an open-source cartesian code to perform crustal and lithospheric-scale simulations. -If you want to learn how to run LaMEM simulations, the easiest way to get started is by looking at [LaMEM.jl](https://github.com/JuliaGeodynamics/LaMEM.jl) which is integrated with `GMG` - -The routines provided here have the following functionality: -- Read LaMEM *.dat files (to get the size of the domain) -- Read LaMEM processor partitioning file -- Save LaMEM marker files in serial or in parallel -- Read a LaMEM timestep - -```@docs -GeophysicalModelGenerator.read_LaMEM_inputfile -GeophysicalModelGenerator.get_processor_partitioning -GeophysicalModelGenerator.save_LaMEM_topography -GeophysicalModelGenerator.save_LaMEM_markers_parallel -GeophysicalModelGenerator.read_data_PVTR -GeophysicalModelGenerator.LaMEM_grid -GeophysicalModelGenerator.create_partitioning_file -``` diff --git a/docs/src/man/tutorial_lamem_post_processing.md b/docs/src/man/tutorial_lamem_post_processing.md index a8b73bea4..048774a8e 100644 --- a/docs/src/man/tutorial_lamem_post_processing.md +++ b/docs/src/man/tutorial_lamem_post_processing.md @@ -45,7 +45,7 @@ tracer = get_tracer_timestep(model_path,timestep,FileName_pvtu) ``` ## 2. Save model information -To analyse the differences between models and its evolution, coordinates of specific phases can be obtained as a matrix. This matrix, together with the corresponding grid configuration and timestep information, provides the basis for extracting field data across all timesteps. Furthermore, surface evolution and tracer distributions can be stored separately for each timestep as welll as the development of a specific grid point. All output is stored in the JLD2 format. +To analyse the differences between models and its evolution, coordinates of specific phases can be obtained as a matrix. This matrix, together with the corresponding grid configuration and timestep information, provides the basis for extracting field data across all timesteps. Furthermore, surface evolution and tracer distributions can be stored separately for each timestep as well as the development of a specific grid point. All output is stored in the JLD2 format. For rapid inspection of model results, snapshots of selected fields can also be generated for each timestep. diff --git a/src/LaMEM_post_processing.jl b/src/LaMEM_post_processing.jl index e44ea089b..cc379dbf4 100644 --- a/src/LaMEM_post_processing.jl +++ b/src/LaMEM_post_processing.jl @@ -11,7 +11,7 @@ using Plots -export search_for_phase_properties, search_for_model_constrains, search_for_all_model_contrains, get_phase, get_phase_bool, split_at__to_type, +export search_for_phase_properties, search_for_model_constrains, search_for_all_model_constrains, get_phase, get_phase_bool, split_at__to_type, get_data_timestep, get_tracer_timestep, get_surf_timestep, post_plot, find_field_properties_grid, find_general_grid_prop, find_surf_evolution, find_tracer_info, track_point_over_time @@ -1102,12 +1102,12 @@ function find_field_properties_grid(data::CartData,subsubfolder_path::String,tim # create matrix from position of indices matrix = get_phase_bool(location,FileName_pvtr,indices) - # write information to dictonary + # write information to dictionary field_info = Dict() # Dictionary to store material properties field_info["indices"]= indices field_info["matrix"]= matrix - # write field information to dictonary for each direction + # write field information to dictionary for each direction for i in eachindex(fields) field_n = Symbol(fields[i]) @@ -1134,7 +1134,7 @@ function find_field_properties_grid(data::CartData,subsubfolder_path::String,tim field_name_dir = string(fields[i]) * string(data_direction[j])# get field direction name field_data = getfield(data.fields, field_n)[j] # get field data direction field_values = field_data[matrix .== 1] # get field of phase - field_info[string(field_name_dir)]= field_values # write to dictonary + field_info[string(field_name_dir)]= field_values # write to dictionary end @@ -1142,7 +1142,7 @@ function find_field_properties_grid(data::CartData,subsubfolder_path::String,tim field_data = getfield(data.fields, field_n)# get field data direction field_values = field_data[matrix .== 1] # get field of phase - field_info[string(fields[i])]= field_values # write to dictonary + field_info[string(fields[i])]= field_values # write to dictionary end end @@ -1409,9 +1409,9 @@ Parameters # load saved field data for one specific time step function load_field_info(timestep::String,output_path::String,output_folder::String) - # load saved field informations + # load saved field information output_slab = joinpath(output_path,output_folder) - output_det = sort(readdir(output_slab), lt=natural) # sort files in output folder according to thte time steps + output_det = sort(readdir(output_slab), lt=natural) # sort files in output folder according to the time steps file_name = filter(s -> occursin(timestep,s), output_det)[1] # read specific times step file diff --git a/src/Setup_geometry.jl b/src/Setup_geometry.jl index c6fcd04cd..f9e47497c 100644 --- a/src/Setup_geometry.jl +++ b/src/Setup_geometry.jl @@ -11,14 +11,49 @@ import Base: show # These are routines that help to create input geometries, such as slabs with a given angle # -export add_box!, add_sphere!, add_ellipsoid!, add_cylinder!, add_layer!, add_polygon!, add_slab!, add_stripes!, add_volcano!, - make_volc_topo, - ConstantTemp, LinearTemp, HalfspaceCoolingTemp, SpreadingRateTemp, LithosphericTemp, LinearWeightedTemperature, - McKenzie_subducting_slab, - ConstantPhase, LithosphericPhases, - Trench, - compute_thermal_structure, compute_phase +export add_box!, add_sphere!, add_ellipsoid!, add_cylinder!, add_layer!, add_polygon!, add_plate!, add_slab!, add_stripes!, add_volcano!, add_fault!, + make_volc_topo, + ConstantTemp, LinearTemp, HalfspaceCoolingTemp, SpreadingRateTemp, LithosphericTemp, LinearWeightedTemperature, + McKenzie_subducting_slab, + ConstantPhase, LithosphericPhases, + Trench, compute_slab_surface, + compute_thermal_structure, compute_phase +""" + ind2D = flatten_index_dimensions(Phase, ind_vec::Vector{CartesianIndex{3}}) + +This converts the indices to purely 2D indices if the array `phase` is 2D +""" +function flatten_index_dimensions(Phase, ind_vec::Vector{CartesianIndex{3}}) + if length(size(Phase)) == 2 + ind2D = Vector{CartesianIndex{2}}(undef, length(ind_vec)) + for (num, ind) in enumerate(ind_vec) + ind2D[num] = CartesianIndex(ind[1], ind[3]) + end + else + ind2D = ind_vec + end + + return ind2D +end + +""" + ind2D = flatten_index_dimensions(Phase, ind_vec::Vector{CartesianIndex{3}}) + +This converts the indices to purely 2D indices if the array `phase` is 2D +""" +function flatten_index_dimensions(Phase::AbstractArray{T, N}, ind_vec::Array{Bool, 3}) where {T, N} + if N == 2 + ind2D = Vector{CartesianIndex{2}}(undef, length(ind_vec)) + for (num, ind) in enumerate(ind_vec) + ind2D[num] = CartesianIndex(ind[1], ind[3]) + end + else + ind2D = ind_vec + end + + return ind2D +end """ add_stripes!(Phase, Grid::AbstractGeneralGrid; @@ -52,9 +87,9 @@ Parameters Example ======== - + Example: Box with striped phase and constant temperature & a dip angle of 10 degrees: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -73,26 +108,28 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para "LaMEM_ModelSetup.vts" ``` """ -function add_stripes!(Phase, Grid::AbstractGeneralGrid; # required input - stripAxes = (1,1,0), # activate stripes along dimensions x, y and z when set to 1 - stripeWidth = 0.2, # full width of a stripe - stripeSpacing = 1, # spacing between two stripes centers - Origin = nothing, # origin - StrikeAngle = 0, # strike - DipAngle = 0, # dip angle - phase = ConstantPhase(3), # phase to be striped - stripePhase = ConstantPhase(4), # stripe phase - cell = false ) # if true, Phase and Temp are defined on cell centers +function add_stripes!( + Phase, Grid::AbstractGeneralGrid; # required input + stripAxes = (1, 1, 0), # activate stripes along dimensions x, y and z when set to 1 + stripeWidth = 0.2, # full width of a stripe + stripeSpacing = 1, # spacing between two stripes centers + Origin = nothing, # origin + StrikeAngle = 0, # strike + DipAngle = 0, # dip angle + phase = ConstantPhase(3), # phase to be striped + stripePhase = ConstantPhase(4), # stripe phase + cell = false + ) # if true, Phase and Temp are defined on cell centers # warnings - if stripeWidth >= stripeSpacing/2.0 + if stripeWidth >= stripeSpacing / 2.0 print("WARNING: stripeWidth should be strictly < stripeSpacing/2.0, otherwise phase is overwritten by the stripePhase\n") elseif sum(stripAxes .== 0) == 3 print("WARNING: at least one axis should be set to 1 e.g. stripAxes = (1,0,0), otherwise no stripes will be added\n") end # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # sets origin if isnothing(Origin) @@ -100,41 +137,41 @@ function add_stripes!(Phase, Grid::AbstractGeneralGrid; # require end # Perform rotation of 3D coordinates: - Xrot = X .- Origin[1]; - Yrot = Y .- Origin[2]; - Zrot = Z .- Origin[3]; + Xrot = X .- Origin[1] + Yrot = Y .- Origin[2] + Zrot = Z .- Origin[3] - Rot3D!(Xrot,Yrot,Zrot, StrikeAngle, DipAngle) + Rot3D!(Xrot, Yrot, Zrot, StrikeAngle, DipAngle) - ph_ind = findall(Phase .== phase.phase); + ph_ind = findall(Phase .== phase.phase) ind = Int64[] if stripAxes[1] == 1 - indX = findall( abs.(Xrot[ph_ind] .% stripeSpacing) .<= stripeWidth/2.0); - ind = vcat(ind,indX); + indX = findall(abs.(Xrot[ph_ind] .% stripeSpacing) .<= stripeWidth / 2.0) + ind = vcat(ind, indX) end if stripAxes[2] == 1 - indY = findall( abs.(Yrot[ph_ind] .% stripeSpacing) .<= stripeWidth/2.0); - ind = vcat(ind,indY); + indY = findall(abs.(Yrot[ph_ind] .% stripeSpacing) .<= stripeWidth / 2.0) + ind = vcat(ind, indY) end if stripAxes[3] == 1 - indZ = findall( abs.(Zrot[ph_ind] .% stripeSpacing) .<= stripeWidth/2.0); - ind = vcat(ind,indZ); + indZ = findall(abs.(Zrot[ph_ind] .% stripeSpacing) .<= stripeWidth / 2.0) + ind = vcat(ind, indZ) end - Phase[ph_ind[ind]] .= stripePhase.phase; - + Phase[ph_ind[ind]] .= stripePhase.phase + return nothing end - """ - add_box!(Phase, Temp, Grid::AbstractGeneralGrid; xlim::NTuple{2, _T} = (20,100), [ylim::NTuple{2, _T} = (1,10)], zlim::NTuple{2, _T} = (10,80), + add_box!(Phase, Temp, Grid::AbstractGeneralGrid; xlim::Tuple = (20,100), [ylim::Tuple = (1,10)], zlim::Tuple = (10,80), Origin=nothing, StrikeAngle=0, DipAngle=0, phase = ConstantPhase(1), T=nothing, - cell=false ) where _T + segments=nothing, + cell=false ) Adds a box with phase & temperature structure to a 3D model setup. This simplifies creating model geometries in geodynamic models @@ -152,13 +189,14 @@ Parameters - `DipAngle` - dip angle of slab - `phase` - specifies the phase of the box. See `ConstantPhase()`,`LithosphericPhases()` - `T` - specifies the temperature of the box. See `ConstantTemp()`,`LinearTemp()`,`HalfspaceCoolingTemp()`,`SpreadingRateTemp()`,`LithosphericTemp()` +- `segments` - optional parameter to define multiple ridge segments within the box - `cell` - if true, `Phase` and `Temp` are defined on centers Examples ======== Example 1) Box with constant phase and temperature & a dip angle of 10 degrees: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -177,7 +215,7 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para ``` Example 2) Box with halfspace cooling profile -```julia +```julia-repl julia> Grid = CartData(xyz_grid(-1000:10:1000,0,-660:10:0)) julia> Phases = zeros(Int32, size(Grid)); julia> Temp = zeros(Float64, size(Grid)); @@ -187,29 +225,46 @@ julia> write_paraview(Grid,"LaMEM_ModelSetup") # Save model to paraview 1-element Vector{String}: "LaMEM_ModelSetup.vts" ``` + +Example 3) Box with ridge thermal structure +```julia-repl +julia> Grid = CartData(xyz_grid(-1000:10:1000, -1000:10:1000, -660:5:0)) +julia> Phases = fill(2, size(Grid)); +julia> Temp = fill(1350.0, size(Grid)); +julia> segments = [((-500.0, -1000.0), (-500.0, 0.0)), + ((-250.0, 0.0), (-250.0, 200.0)), + ((-750.0, 200.0), (-750.0, 1000.0))]; +julia> lith = LithosphericPhases(Layers=[15 55], Phases=[1 2], Tlab=1250); +julia> add_box!(Phases, Temp, Grid; xlim=(-1000.0, 0.0), ylim=(-500.0, 500.0), + zlim=(-80.0, 0.0), phase=lith, + T=SpreadingRateTemp(SpreadingVel=3), segments=segments) +julia> Grid = addfield(Grid, (; Phases, Temp)); # Add to Cartesian model +julia> write_paraview(Grid, "Ridge_Thermal_Structure") # Save model to Paraview +1-element Vector{String}: + "Ridge_Thermal_Structure.vts" """ -function add_box!(Phase, Temp, Grid::AbstractGeneralGrid; # required input - xlim::NTuple{2, _T} = (20,100), ylim=nothing, zlim::NTuple{2, _T} = (10,80), # limits of the box - Origin=nothing, StrikeAngle=0, DipAngle=0, # origin & dip/strike - phase = ConstantPhase(1), # Sets the phase number(s) in the box - T=nothing, # Sets the thermal structure (various functions are available) - cell=false ) where _T # if true, Phase and Temp are defined on cell centers +function add_box!( + Phase, Temp, Grid::AbstractGeneralGrid; # required input + xlim::Tuple = (20, 100), ylim = nothing, zlim::Tuple = (10, 80), # limits of the box + Origin = nothing, StrikeAngle = 0, DipAngle = 0, # origin & dip/strike + phase = ConstantPhase(1), # Sets the phase number(s) in the box + T = nothing, # Sets the thermal structure (various functions are available) + segments = nothing, # Allows defining multiple ridge segments + cell = false + ) # if true, Phase and Temp are defined on cell centers + # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # ensure that the input arrays have the correct size - @assert size(X) == size(Phase) == size(Temp) + #@assert size(X) == size(Phase) == size(Temp) # Limits of block - if ylim==nothing + if ylim == nothing ylim = (minimum(Y), maximum(Y)) end - if Origin==nothing - Origin = (xlim[1], ylim[1], zlim[2]) # upper-left corner - end - if Origin !== nothing && isa(T, McKenzie_subducting_slab) @warn "McKenzie temperature does not require the definition of 'Origin' field; if Origin is defined it must be equal to [xmin,ymin,zmax] of the box that has been defined." if Origin[1] != xlim[1] || Origin[2] != ylim[1] || Origin[3] != zlim[2] @@ -217,39 +272,93 @@ function add_box!(Phase, Temp, Grid::AbstractGeneralGrid; # required input end end + if Origin == nothing + Origin = (xlim[1], ylim[1], zlim[2]) # upper-left corner + end + # Perform rotation of 3D coordinates: - Xrot = X .- Origin[1]; - Yrot = Y .- Origin[2]; - Zrot = Z .- Origin[3]; + Xrot = X .- Origin[1] + Yrot = Y .- Origin[2] + Zrot = Z .- Origin[3] - Rot3D!(Xrot,Yrot,Zrot, StrikeAngle, DipAngle) + Rot3D!(Xrot, Yrot, Zrot, StrikeAngle, DipAngle) # Set phase number & thermal structure in the full domain ztop = maximum(zlim) - Origin[3] zbot = minimum(zlim) - Origin[3] - ind = findall( (Xrot .>= (minimum(xlim) - Origin[1])) .& (Xrot .<= (maximum(xlim) - Origin[1])) .& - (Yrot .>= (minimum(ylim) - Origin[2])) .& (Yrot .<= (maximum(ylim) - Origin[2])) .& - (Zrot .>= zbot) .& (Zrot .<= ztop) ) + ind = findall( + (Xrot .>= (minimum(xlim) - Origin[1])) .& (Xrot .<= (maximum(xlim) - Origin[1])) .& + (Yrot .>= (minimum(ylim) - Origin[2])) .& (Yrot .<= (maximum(ylim) - Origin[2])) .& + (Zrot .>= zbot) .& (Zrot .<= ztop) + ) - # Compute thermal structure accordingly. See routines below for different options - if T != nothing - if isa(T,LithosphericTemp) - Phase[ind] = compute_phase(Phase[ind], Temp[ind], Xrot[ind], Yrot[ind], Zrot[ind], phase) + ind_flat = flatten_index_dimensions(Phase, ind) + + if !isempty(ind_flat) + # Compute thermal structure accordingly. See routines below for different options + if T != nothing + if isa(T, LithosphericTemp) + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], Xrot[ind], Yrot[ind], Zrot[ind], phase) + end + if segments !== nothing + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], Xrot[ind], Yrot[ind], Zrot[ind], Phase[ind_flat], T, segments) + else + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], Xrot[ind], Yrot[ind], Zrot[ind], Phase[ind_flat], T) + end end - Temp[ind] = compute_thermal_structure(Temp[ind], Xrot[ind], Yrot[ind], Zrot[ind], Phase[ind], T) + # Set the phase. Different routines are available for that - see below. + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], Xrot[ind], Yrot[ind], Zrot[ind], phase) end - # Set the phase. Different routines are available for that - see below. - Phase[ind] = compute_phase(Phase[ind], Temp[ind], Xrot[ind], Yrot[ind], Zrot[ind], phase) - return nothing end +""" + add_box!(Phase, Temp, Grid::AbstractGeneralGrid, + bounds::AbstractVector{<:AbstractVector{<:Real}}; + Origin = nothing, StrikeAngle = 0, DipAngle = 0, + phase = ConstantPhase(1), + T = nothing, + segments = nothing, + cell = false ) +Add box function but getting bounds as a vector bounds in a way of [[xmin,xmax],[ymin,ymax],[zmin,zmax]]. If bounds is empty return nothing +""" +function add_box!( + Phase, Temp, Grid::AbstractGeneralGrid, # required input + bounds::Union{Vector{Any}, AbstractVector{<:AbstractVector{<:Real}}}; # limits of the box + Origin = nothing, StrikeAngle = 0, DipAngle = 0, # origin & dip/strike + phase = ConstantPhase(1), # Sets the phase number(s) in the box + T = nothing, # Sets the thermal structure (various functions are available) + segments = nothing, # Allows defining multiple ridge segments + cell = false # if true, Phase and Temp are defined on cell centers + ) + + if isempty(bounds) + + return nothing + + else + + xlim = (Tuple(bounds[1])) + ylim = (Tuple(bounds[2])) + zlim = (Tuple(bounds[3])) + + add_box!( + Phase, Temp, Grid; # required input + xlim = xlim, ylim = ylim, zlim = zlim, # limits of the box + Origin = Origin, StrikeAngle = StrikeAngle, DipAngle = DipAngle, # origin & dip/strike + phase = phase, # Sets the phase number(s) in the box + T = T, # Sets the thermal structure (various functions are available) + cell = cell + ) + end + +end """ - add_layer!(Phase, Temp, Grid::AbstractGeneralGrid; xlim::NTuple{2, _T} = (1,100), [ylim::NTuple{2, _T} = (0,20)], zlim::NTuple{2, _T} = (0,-100), + add_layer!(Phase, Temp, Grid::AbstractGeneralGrid; xlim::Tuple = (1,100), [ylim::Tuple = (0,20)], zlim::Tuple = (0,-100), phase = ConstantPhase(1), - T=nothing, cell=false ) where _T + T=nothing, cell=false ) Adds a layer with phase & temperature structure to a 3D model setup. The most common use would be to add a lithospheric layer to a model setup. @@ -272,7 +381,7 @@ Examples ======== Example 1) Layer with constant phase and temperature -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -291,7 +400,7 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para ``` Example 2) Box with halfspace cooling profile -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") julia> Phases = zeros(Int32, size(Grid.X)); julia> Temp = zeros(Float64, size(Grid.X)); @@ -302,17 +411,19 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para "LaMEM_ModelSetup.vts" ``` """ -function add_layer!(Phase, Temp, Grid::AbstractGeneralGrid; # required input - xlim=nothing, ylim=nothing, zlim=nothing, # limits of the layer - phase = ConstantPhase(1), # Sets the phase number(s) in the box - T=nothing, # Sets the thermal structure (various functions are available) - cell = false ) # if true, Phase and Temp are defined on cell centers - +function add_layer!( + Phase, Temp, Grid::AbstractGeneralGrid; # required input + xlim = nothing, ylim = nothing, zlim = nothing, # limits of the layer + phase = ConstantPhase(1), # Sets the phase number(s) in the box + T = nothing, # Sets the thermal structure (various functions are available) + cell = false + ) # if true, Phase and Temp are defined on cell centers + # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # Limits of block - if isnothing(xlim)==isnothing(ylim)==isnothing(zlim) + if isnothing(xlim) == isnothing(ylim) == isnothing(zlim) error("You need to specify at least one of the limits (xlim, ylim, zlim)") end @@ -327,31 +438,32 @@ function add_layer!(Phase, Temp, Grid::AbstractGeneralGrid; # required input end # Set phase number & thermal structure in the full domain - ind = findall( (X .>= (xlim[1])) .& (X .<= (xlim[2])) .& - (Y .>= (ylim[1])) .& (Y .<= (ylim[2])) .& - (Z .>= (zlim[1])) .& (Z .<= (zlim[2])) - ) + ind = findall( + (X .>= (xlim[1])) .& (X .<= (xlim[2])) .& + (Y .>= (ylim[1])) .& (Y .<= (ylim[2])) .& + (Z .>= (zlim[1])) .& (Z .<= (zlim[2])) + ) + ind_flat = flatten_index_dimensions(Phase, ind) - # Compute thermal structure accordingly. See routines below for different options - if !isnothing(T) - Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T) - end + if !isempty(ind_flat) + # Compute thermal structure accordingly. See routines below for different options + if !isnothing(T) + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], X[ind], Y[ind], Z[ind], Phase[ind_flat], T) + end - # Set the phase. Different routines are available for that - see below. - Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + # Set the phase. Different routines are available for that - see below. + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], X[ind], Y[ind], Z[ind], phase) + end return nothing end - - - """ - add_sphere!(Phase, Temp, Grid::AbstractGeneralGrid; cen::NTuple{3, _T} = (0,0,-1), radius::Number, + add_sphere!(Phase, Temp, Grid::AbstractGeneralGrid; cen::Tuple = (0,0,-1), radius::Number, phase = ConstantPhase(1). - T=nothing, cell=false ) where _T + T=nothing, cell=false ) Adds a sphere with phase & temperature structure to a 3D model setup. This simplifies creating model geometries in geodynamic models @@ -373,7 +485,7 @@ Example ======== Sphere with constant phase and temperature: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -391,33 +503,39 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para "LaMEM_ModelSetup.vts" ``` """ -function add_sphere!(Phase, Temp, Grid::AbstractGeneralGrid; # required input - cen::NTuple{3, _T} = (0,0,-1), radius::Number, # center and radius of the sphere - phase = ConstantPhase(1), # Sets the phase number(s) in the sphere - T=nothing, cell=false ) where _T # Sets the thermal structure (various functions are available) +function add_sphere!( + Phase, Temp, Grid::AbstractGeneralGrid; # required input + cen::Tuple = (0, 0, -1), radius::Number, # center and radius of the sphere + phase = ConstantPhase(1), # Sets the phase number(s) in the sphere + T = nothing, cell = false + ) # Sets the thermal structure (various functions are available) # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # Set phase number & thermal structure in the full domain - ind = findall(((X .- cen[1]).^2 + (Y .- cen[2]).^2 + (Z .- cen[3]).^2).^0.5 .< radius) + ind = findall(((X .- cen[1]) .^ 2 + (Y .- cen[2]) .^ 2 + (Z .- cen[3]) .^ 2) .^ 0.5 .< radius) - # Compute thermal structure accordingly. See routines below for different options - if T != nothing - Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T) - end + ind_flat = flatten_index_dimensions(Phase, ind) - # Set the phase. Different routines are available for that - see below. - Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + if !isempty(ind_flat) + # Compute thermal structure accordingly. See routines below for different options + if T != nothing + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], X[ind], Y[ind], Z[ind], Phase[ind_flat], T) + end + + # Set the phase. Different routines are available for that - see below. + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], X[ind], Y[ind], Z[ind], phase) + end return nothing end """ - add_ellipsoid!(Phase, Temp, Grid::AbstractGeneralGrid; cen::NTuple{3, _T} = (-1,-1,-1), axes::NTuple{3, _T} = (0.2,0.1,0.5), + add_ellipsoid!(Phase, Temp, Grid::AbstractGeneralGrid; cen::Tuple = (-1,-1,-1), axes::Tuple = (0.2,0.1,0.5), Origin=nothing, StrikeAngle=0, DipAngle=0, phase = ConstantPhase(1). - T=nothing, cell=false ) where _T + T=nothing, cell=false ) Adds an Ellipsoid with phase & temperature structure to a 3D model setup. This simplifies creating model geometries in geodynamic models @@ -440,7 +558,7 @@ Example ======== Ellipsoid with constant phase and temperature, rotated 90 degrees and tilted by 45 degrees: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -458,49 +576,59 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para "LaMEM_ModelSetup.vts" ``` """ -function add_ellipsoid!(Phase, Temp, Grid::AbstractGeneralGrid; # required input - cen::NTuple{3, _T} = (-1,-1,-1), axes::NTuple{3, _T} = (0.2,0.1,0.5), # center and semi-axes of the ellpsoid - Origin=nothing, StrikeAngle=0, DipAngle=0, # origin & dip/strike - phase = ConstantPhase(1), # Sets the phase number(s) in the box - T=nothing, cell=false ) where _T # Sets the thermal structure (various functions are available) - - if Origin==nothing +function add_ellipsoid!( + Phase, Temp, Grid::AbstractGeneralGrid; # required input + cen::Tuple = (-1, -1, -1), axes::Tuple = (0.2, 0.1, 0.5), # center and semi-axes of the ellpsoid + Origin = nothing, StrikeAngle = 0, DipAngle = 0, # origin & dip/strike + phase = ConstantPhase(1), # Sets the phase number(s) in the box + T = nothing, cell = false + ) # Sets the thermal structure (various functions are available) + + if Origin == nothing Origin = cen # center end # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # Perform rotation of 3D coordinates: - Xrot = X .- Origin[1]; - Yrot = Y .- Origin[2]; - Zrot = Z .- Origin[3]; + Xrot = X .- Origin[1] + Yrot = Y .- Origin[2] + Zrot = Z .- Origin[3] - Rot3D!(Xrot,Yrot,Zrot, StrikeAngle, DipAngle) + Rot3D!(Xrot, Yrot, Zrot, StrikeAngle, DipAngle) # Set phase number & thermal structure in the full domain - x2 = axes[1]^2 - y2 = axes[2]^2 - z2 = axes[3]^2 + x2 = axes[1]^2 + y2 = axes[2]^2 + z2 = axes[3]^2 cenRot = cen .- Origin - ind = findall((((Xrot .- cenRot[1]).^2)./x2 + ((Yrot .- cenRot[2]).^2)./y2 + - ((Zrot .- cenRot[3]).^2)./z2) .^0.5 .<= 1) + ind = findall( + ( + ((Xrot .- cenRot[1]) .^ 2) ./ x2 + ((Yrot .- cenRot[2]) .^ 2) ./ y2 + + ((Zrot .- cenRot[3]) .^ 2) ./ z2 + ) .^ 0.5 .<= 1 + ) - # Compute thermal structure accordingly. See routines below for different options - if T != nothing - Temp[ind] = compute_thermal_structure(Temp[ind], Xrot[ind], Yrot[ind], Zrot[ind], Phase[ind], T) - end + ind_flat = flatten_index_dimensions(Phase, ind) - # Set the phase. Different routines are available for that - see below. - Phase[ind] = compute_phase(Phase[ind], Temp[ind], Xrot[ind], Yrot[ind], Zrot[ind], phase) + if !isempty(ind_flat) + # Compute thermal structure accordingly. See routines below for different options + if T != nothing + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], Xrot[ind], Yrot[ind], Zrot[ind], Phase[ind_flat], T) + end + + # Set the phase. Different routines are available for that - see below. + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], Xrot[ind], Yrot[ind], Zrot[ind], phase) + end return nothing end """ - add_cylinder!(Phase, Temp, Grid::AbstractGeneralGrid; base::NTuple{3, _T} = (-1,-1,-1.5), cap::NTuple{3, _T} = (-1,-1,-0.5), radius::Number, + add_cylinder!(Phase, Temp, Grid::AbstractGeneralGrid; base::Tuple = (-1,-1,-1.5), cap::Tuple = (-1,-1,-0.5), radius::Number, phase = ConstantPhase(1), - T=nothing, cell=false ) where _T + T=nothing, cell=false ) Adds a cylinder with phase & temperature structure to a 3D model setup. This simplifies creating model geometries in geodynamic models @@ -510,7 +638,7 @@ Parameters ==== - `Phase` - Phase array (consistent with Grid) - `Temp` - Temperature array (consistent with Grid) -- `Grid` - Grid structure (usually obtained with read_LaMEM_inputfile) +- `Grid` - Grid structure (usually obtained with `read_LaMEM_inputfile`) - `base` - center coordinate of bottom of cylinder - `cap` - center coordinate of top of cylinder - `radius` - radius of the cylinder @@ -523,7 +651,7 @@ Example ======== Cylinder with constant phase and temperature: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -541,51 +669,57 @@ julia> write_paraview(Model3D,"LaMEM_ModelSetup") # Save model to para "LaMEM_ModelSetup.vts" ``` """ -function add_cylinder!(Phase, Temp, Grid::AbstractGeneralGrid; # required input - base::NTuple{3, _T} = (-1,-1,-1.5), cap::NTuple{3, _T} = (-1,-1,-0.5), radius::Number, # center and radius of the sphere - phase = ConstantPhase(1), # Sets the phase number(s) in the sphere - T=nothing, cell=false ) where _T # Sets the thermal structure (various functions are available) +function add_cylinder!( + Phase, Temp, Grid::AbstractGeneralGrid; # required input + base::Tuple = (-1, -1, -1.5), cap::Tuple = (-1, -1, -0.5), radius::Number, # center and radius of the sphere + phase = ConstantPhase(1), # Sets the phase number(s) in the sphere + T = nothing, cell = false + ) # Sets the thermal structure (various functions are available) # axis vector of cylinder axVec = cap .- base - ax2 = (axVec[1]^2 + axVec[2]^2 + axVec[3]^2) + ax2 = (axVec[1]^2 + axVec[2]^2 + axVec[3]^2) # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # distance between grid points and cylinder base - dx_b = X .- base[1] - dy_b = Y .- base[2] - dz_b = Z .- base[3] + dx_b = X .- base[1] + dy_b = Y .- base[2] + dz_b = Z .- base[3] # find normalized parametric coordinate of a point-axis projection - t = (axVec[1] .* dx_b .+ axVec[2] .* dy_b .+ axVec[3] .* dz_b) ./ ax2 + t = (axVec[1] .* dx_b .+ axVec[2] .* dy_b .+ axVec[3] .* dz_b) ./ ax2 # find distance vector between point and axis - dx = dx_b .- t.*axVec[1] - dy = dy_b .- t.*axVec[2] - dz = dz_b .- t.*axVec[3] + dx = dx_b .- t .* axVec[1] + dy = dy_b .- t .* axVec[2] + dz = dz_b .- t .* axVec[3] # Set phase number & thermal structure in the full domain - ind = findall((t .>= 0.0) .& (t .<= 1.0) .& ((dx.^2 + dy.^2 + dz.^2).^0.5 .<= radius)) + ind = findall((t .>= 0.0) .& (t .<= 1.0) .& ((dx .^ 2 + dy .^ 2 + dz .^ 2) .^ 0.5 .<= radius)) - # Compute thermal structure accordingly. See routines below for different options - if T != nothing - Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T) - end + ind_flat = flatten_index_dimensions(Phase, ind) - # Set the phase. Different routines are available for that - see below. - Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + if !isempty(ind_flat) + # Compute thermal structure accordingly. See routines below for different options + if !isnothing(T) + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], X[ind], Y[ind], Z[ind], Phase[ind_flat], T) + end + + # Set the phase. Different routines are available for that - see below. + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], X[ind], Y[ind], Z[ind], phase) + end return nothing end # Internal function that rotates the coordinates -function Rot3D!(X,Y,Z, StrikeAngle, DipAngle) +function Rot3D!(X, Y, Z, StrikeAngle, DipAngle) # precompute trigonometric functions (expensive!) - sindStrikeAngle, cosStrikeAngle = sincosd(StrikeAngle) - sinDipAngle, cosDipAngle = sincosd(-DipAngle) # note the minus here to be consistent with the earlier version of the code + sindStrikeAngle, cosStrikeAngle = sincosd(StrikeAngle) + sinDipAngle, cosDipAngle = sincosd(-DipAngle) # note the minus here to be consistent with the earlier version of the code for i in eachindex(X) X[i], Y[i], Z[i] = Rot3D(X[i], Y[i], Z[i], cosStrikeAngle, sindStrikeAngle, cosDipAngle, sinDipAngle) end @@ -594,9 +728,8 @@ function Rot3D!(X,Y,Z, StrikeAngle, DipAngle) end - """ - add_polygon!(Phase, Temp, Grid::AbstractGeneralGrid; xlim=(), ylim::NTuple{2, _T} = (0,0.8), zlim=(), phase = ConstantPhase(1), T=nothing, cell=false ) where _T + add_polygon!(Phase, Temp, Grid::AbstractGeneralGrid; xlim=(), ylim::Tuple = (0.0,0.8), zlim=(), phase = ConstantPhase(1), T=nothing, cell=false ) Adds a polygon with phase & temperature structure to a 3D model setup. This simplifies creating model geometries in geodynamic models @@ -618,7 +751,7 @@ Example Polygon with constant phase and temperature: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -648,52 +781,206 @@ function add_polygon!(Phase, Temp, Grid::AbstractGeneralGrid; # required input zlim_ = Float64.(collect(zlim)) -# Retrieve 3D data arrays for the grid -X,Y,Z = coordinate_grids(Grid, cell=cell) + # Retrieve 3D data arrays for the grid + X,Y,Z = coordinate_grids(Grid, cell=cell) -ind = zeros(Bool,size(X)) -ind_slice = zeros(Bool,size(X[:,1,:])) + ind = zeros(Bool,size(X)) + ind_slice = zeros(Bool,size(X[:,1,:])) -# find points within the polygon, only in 2D -for i = 1:size(Y)[2] - if Y[1,i,1] >= ylim_[1] && Y[1,i,1]<=ylim_[2] - inpolygon!(ind_slice, xlim_,zlim_, X[:,i,:], Z[:,i,:]) - ind[:,i,:] = ind_slice - else - ind[:,i,:] = zeros(size(X[:,1,:])) + # find points within the polygon, only in 2D + for i = 1:size(Y)[2] + if Y[1,i,1] >= ylim_[1] && Y[1,i,1]<=ylim_[2] + inpolygon!(ind_slice, xlim_,zlim_, X[:,i,:], Z[:,i,:]) + ind[:,i,:] = ind_slice + else + ind[:,i,:] = zeros(size(X[:,1,:])) + end + end + + Zrel = Z .- maximum(zlim); + + println("add poly") + # Compute thermal structure accordingly. See routines below for different options + if T != nothing + Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Zrel[ind], Phase[ind], T) end + + # Set the phase. Different routines are available for that - see below. + Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + +return nothing end -Zrel = Z .- maximum(zlim); +""" + add_polygon!(Phase, Temp, Grid::AbstractGeneralGrid, + bounds::AbstractVector{<:AbstractVector{<:Real}}; + phase = ConstantPhase(1), + T = nothing, + cell = false ) +Add polygon function but getting bounds as a vector bounds in a way of [[xmin,xmax],[ymin,ymax],[zmin,zmax]]. If bounds is empty return nothing +""" +function add_polygon!( + Phase, Temp, Grid::AbstractGeneralGrid, # required input + bounds::Union{Vector{Any}, AbstractVector{<:AbstractVector{<:Real}}}; # limits of the box + phase = ConstantPhase(1), # Sets the phase number(s) in the box + T = nothing, # Sets the thermal structure (various functions are available) + cell = false # if true, Phase and Temp are defined on cell centers + ) + + return if !isempty(bounds) + xlim = (Tuple(bounds[1])) + ylim = (Tuple(bounds[2])) + zlim = (Tuple(bounds[3])) + + add_polygon!( + Phase, Temp, Grid; # required input + xlim = xlim, ylim = ylim, zlim = zlim, # limits of the box + phase = phase, # Sets the phase number(s) in the box + T = T, # Sets the thermal structure (various functions are available) + cell = cell + ) + end -println("add poly") -# Compute thermal structure accordingly. See routines below for different options -if T != nothing - Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Zrel[ind], Phase[ind], T) end -# Set the phase. Different routines are available for that - see below. -Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) +""" + add_plate!(Phase, Temp, Grid::AbstractGeneralGrid; xlim=(), ylim=(), zlim::Tuple = (0.0,0.8), phase = ConstantPhase(1), T=nothing, segments=nothing, cell=false ) +Adds a tectonic plate with phase and temperature structure to a 3D model setup. +This function enables the definition of tectonic plates in the xy plane and projects them along the z-axis, providing a flexible approach to model complex plate geometries. +Parameters +========== +- `Phase` - Phase array (consistent with Grid) +- `Temp` - Temperature array (consistent with Grid) +- `Grid` - Grid structure (usually obtained with read_LaMEM_inputfile) +- `xlim` - `x`-coordinate of the polygon points, same ordering as ylim, number of points unlimited +- `ylim` - `y`-coordinate of the polygon points, same ordering as xlim, number of points unlimited +- `zlim` - `z`-coordinate range for projecting the polygon (start and stop, two values) +- `phase` - Specifies the phase of the plate. See `ConstantPhase()` +- `T` - Specifies the temperature of the plate. See `ConstantTemp()`, `LinearTemp()`, `HalfspaceCoolingTemp()`, `SpreadingRateTemp()` +- `segments` - Optional. Allows for thermal segmentation within the polygon. Useful for ridge systems or complex thermal structures. +- `cell` - If true, `Phase` and `Temp` are defined on cell centers +Example +======== +Tectonic plate in the xy plane with phase and temperature structure: +```julia-repl +julia> Grid = CartData(xyz_grid(x, y, z)) +Grid: + nel : (512, 512, 128) + marker/cell : (1, 1, 1) + markers : (512, 512, 128) + x ϵ [-1000.0 : 0.0] + y ϵ [-1000.0 : 1000.0] + z ϵ [-660.0 : 0.0] +julia> Phases = zeros(Int32, size(Grid.X)) +julia> Temp = zeros(Float64, size(Grid.X)) +julia> segments = [ + ((-500.0, -1000.0), (-500.0, 0.0)), # Segment 1 + ((-250.0, 0.0), (-250.0, 200.0)), # Segment 2 + ((-750.0, 200.0), (-750.0, 1000.0)) # Segment 3 + ] +julia> lith = LithosphericPhases(Layers=[15 55], Phases=[1 2], Tlab=1250) +julia> add_plate!(Phases, Temp, Grid; + xlim=(-1000.0, -750.0, -250.0, 0.0, -250.0, -750.0), + ylim=(0.0, 500.0, 500.0, 0.0, -500.0, -500.0), + zlim=(-150.0, 0.0), + phase=lith, + T=SpreadingRateTemp(SpreadingVel=3), + segments=segments) +julia> Grid = addfield(Grid, (; Phases, Temp)) # Add fields +julia> write_paraview(Grid, "Plate") # Save model to Paraview +1-element Vector{String}: + "Plate.vts" +""" + +function add_plate!( + Phase, Temp, Grid::AbstractGeneralGrid; + xlim = (), ylim = (), zlim::Tuple = (0.0, 0.8), + phase = ConstantPhase(1), + T = nothing, segments = nothing, cell = false + ) -return nothing + xlim_ = collect(xlim) + ylim_ = collect(ylim) + zlim_ = collect(zlim) + + X, Y, Z = coordinate_grids(Grid, cell = cell) + ind = zeros(Bool, size(X)) + ind_slice = zeros(Bool, size(X[:, :, 1])) + + for k in 1:size(Z, 3) + if zlim_[1] <= Z[1, 1, k] <= zlim_[2] + inpolygon!(ind_slice, xlim_, ylim_, X[:, :, k], Y[:, :, k]) + @views ind[:, :, k] = ind_slice + else + @views ind[:, :, k] = zeros(size(X[:, :, 1])) + end + end + + if !isempty(ind) + if T != nothing + if segments !== nothing + Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T, segments) + else + Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T) + end + end + Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + end + + return nothing +end + +""" + add_plate!(Phase, Temp, Grid::AbstractGeneralGrid, + bounds::AbstractVector{<:AbstractVector{<:Real}}; + phase = ConstantPhase(1), + T = nothing, + segments = nothing, + cell = false ) +Add plate function but getting bounds as a vector bounds in a way of [[xmin,xmax],[ymin,ymax],[zmin,zmax]]. If bounds is empty return nothing +""" +function add_plate!( + Phase, Temp, Grid::AbstractGeneralGrid, # required input + bounds::Union{Vector{Any}, AbstractVector{<:AbstractVector{<:Real}}}; # limits of the box + phase = ConstantPhase(1), # Sets the phase number(s) in the box + T = nothing, # Sets the thermal structure (various functions are available) + segments = nothing, # Allows defining multiple ridge segments + cell = false # if true, Phase and Temp are defined on cell centers + ) + + return if !isempty(bounds) + xlim = (Tuple(bounds[1])) + ylim = (Tuple(bounds[2])) + zlim = (Tuple(bounds[3])) + + add_plate!( + Phase, Temp, Grid; # required input + xlim = xlim, ylim = ylim, zlim = zlim, # limits of the box + Origin = Origin, StrikeAngle = StrikeAngle, DipAngle = DipAngle, # origin & dip/strike + phase = phase, # Sets the phase number(s) in the box + T = T, # Sets the thermal structure (various functions are available) + segments = segments, # Allows defining multiple ridge segments + cell = cell + ) + end end """ xrot, yrot, zrot = Rot3D(X::Number,Y::Number,Z::Number, cosStrikeAngle, sindStrikeAngle, cosDipAngle, sinDipAngle) -Perform rotation for a point in 3D space + Perform rotation for a point in 3D space """ -function Rot3D(X::_T,Y::_T,Z::_T, cosStrikeAngle::_T, sindStrikeAngle::_T, cosDipAngle::_T, sinDipAngle::_T) where _T<:Number +function Rot3D(X::_T, Y::_T, Z::_T, cosStrikeAngle::_T, sindStrikeAngle::_T, cosDipAngle::_T, sinDipAngle::_T) where {_T <: Number} # rotation matrixes #roty = [cosd(-DipAngle) 0 sind(-DipAngle) ; 0 1 0 ; -sind(-DipAngle) 0 cosd(-DipAngle)]; - roty = @SMatrix [cosDipAngle 0 sinDipAngle ; 0 1 0 ; -sinDipAngle 0 cosDipAngle]; # note that dip-angle is changed from before! - rotz = @SMatrix [cosStrikeAngle -sindStrikeAngle 0 ; sindStrikeAngle cosStrikeAngle 0 ; 0 0 1] + roty = @SMatrix [cosDipAngle 0 sinDipAngle ; 0 1 0 ; -sinDipAngle 0 cosDipAngle] # note that dip-angle is changed from before! + rotz = @SMatrix [cosStrikeAngle -sindStrikeAngle 0 ; sindStrikeAngle cosStrikeAngle 0 ; 0 0 1] + + CoordVec = @SVector [X, Y, Z] + CoordRot = rotz * CoordVec + CoordRot = roty * CoordRot - CoordVec = @SVector [X, Y, Z] - CoordRot = rotz*CoordVec; - CoordRot = roty*CoordRot; - return CoordRot[1], CoordRot[2], CoordRot[3] end @@ -730,34 +1017,35 @@ Optional Parameters - background - this allows loading in a topography and only adding the volcano on top (also allows stacking of several cones to get a volcano with different slopes) """ function add_volcano!( - Phases, - Temp, - Grid::CartData; - volcanic_phase = 1, - center = (0,0,0), - height = 0.0, - radius = 0.0, - crater = 0.0, - base = 0.0, - background = nothing, - T = HalfspaceCoolingTemp(Age=0) -) - H = make_volc_topo(Grid; - center = center, - height = height, - radius = radius, - crater = crater, - base = base, + Phases, + Temp, + Grid::CartData; + volcanic_phase = 1, + center = (0, 0, 0), + height = 0.0, + radius = 0.0, + crater = 0.0, + base = 0.0, + background = nothing, + T = HalfspaceCoolingTemp(Age = 0) + ) + H = make_volc_topo( + Grid; + center = center, + height = height, + radius = radius, + crater = crater, + base = base, background = background ) - ni = size(Grid.x) - ind = fill(false, ni...) + ni = size(Grid.x) + ind = fill(false, ni...) depth = similar(Grid.z.val) for k in axes(ind, 3) for j in axes(ind, 2), i in axes(ind, 1) - depth[i, j, k] = max(H[i, j] - Grid.z.val[i, j, k], 0) + depth[i, j, k] = max(H[i, j] - Grid.z.val[i, j, k], 0) if Grid.z.val[i, j, k] < H[i, j] && Grid.z.val[i, j, k] ≥ base Phases[i, j, k] = volcanic_phase @@ -768,9 +1056,15 @@ function add_volcano!( end end + ind_flat = flatten_index_dimensions(Phases, ind) + # @views Temp[ind .== false] .= 0.0 - @views Temp[ind] .= compute_thermal_structure(Temp[ind], Grid.x.val[ind], Grid.y.val[ind], depth[ind], Phases[ind], T) - + if !isempty(ind_flat) + if !isnothing(T) + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], Grid.x.val[ind], Grid.y.val[ind], depth[ind], Phases[ind_flat], T) + end + end + return nothing end @@ -799,7 +1093,7 @@ Example ======== Cylinder with constant phase and temperature: -```julia +```julia-repl julia> Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") LaMEM Grid: nel : (32, 32, 32) @@ -828,102 +1122,105 @@ julia> write_paraview(Topo,"VolcanoTopo") # Save topography to paravie Saved file: VolcanoTopo.vts ``` """ -function make_volc_topo(Grid::LaMEM_grid; - center::Array{Float64, 1}, - height::Float64, - radius::Float64, - crater=0.0, - base=0.0, - background=nothing) +function make_volc_topo( + Grid::LaMEM_grid; + center::Array{Float64, 1}, + height::Float64, + radius::Float64, + crater = 0.0, + base = 0.0, + background = nothing + ) # create nondimensionalization object - CharUnits = SI_units(length=1000m); + CharUnits = SI_units(length = 1000m) # get node grid - X = Grid.Xn[:,:,1]; - Y = Grid.Yn[:,:,1]; - nx = size(X,1); - ny = size(X,2); + X = Grid.Xn[:, :, 1] + Y = Grid.Yn[:, :, 1] + nx = size(X, 1) + ny = size(X, 2) # compute radial distance to volcano center - DX = X .- center[1] - DY = Y .- center[2] - RD = (DX.^2 .+ DY.^2).^0.5 + DX = X .- center[1] + DY = Y .- center[2] + RD = (DX .^ 2 .+ DY .^ 2) .^ 0.5 # get radial distance from crater rim RD .-= crater # find position relative to crater rim - dr = radius - crater - pos = (-RD ./ dr .+ 1) + dr = radius - crater + pos = (-RD ./ dr .+ 1) ## assign topography - H = zeros(Float64, (nx,ny)) + H = zeros(Float64, (nx, ny)) # check if there is a background supplied if background === nothing - H .= base + H .= base else background = nondimensionalize(background, CharUnits) if size(background) == size(X) H .= background - elseif size(background) == size(reshape(X,nx,ny,1)) - H .= background[:,:,1] + elseif size(background) == size(reshape(X, nx, ny, 1)) + H .= background[:, :, 1] else error("Size of background must be ", string(nx), "x", string(ny)) end end - ind = findall(x->0.0<=x<1.0, pos) - H[ind] .= pos[ind] .* (height-base) .+ base - ind = findall(x->x>= 1.0, pos) + ind = findall(x -> 0.0 <= x < 1.0, pos) + H[ind] .= pos[ind] .* (height - base) .+ base + ind = findall(x -> x >= 1.0, pos) H[ind] .= height # dimensionalize Topo = dimensionalize(H, km, CharUnits) # build and return CartData - return CartData(reshape(X,nx,ny,1), reshape(Y,nx,ny,1), reshape(Topo,nx,ny,1), (Topography=reshape(Topo,nx,ny,1),)) + return CartData(reshape(X, nx, ny, 1), reshape(Y, nx, ny, 1), reshape(Topo, nx, ny, 1), (Topography = reshape(Topo, nx, ny, 1),)) end -function make_volc_topo(Grid::CartData; - center = (0,0,0), - height = 0.0, - radius = 0.0, - crater = 0.0, - base = 0.0, - background = nothing -) +function make_volc_topo( + Grid::CartData; + center = (0, 0, 0), + height = 0.0, + radius = 0.0, + crater = 0.0, + base = 0.0, + background = nothing + ) # get node grid - X = @views Grid.x.val[:,:,1] - Y = @views Grid.y.val[:,:,1] - nx = size(X, 1) - ny = size(X, 2) - pos = similar(X) + X = @views Grid.x.val[:, :, 1] + Y = @views Grid.y.val[:, :, 1] + nx = size(X, 1) + ny = size(X, 2) + pos = similar(X) for i in eachindex(pos) # compute radial distance to volcano center - DX = X[i] - center[1] - DY = Y[i] - center[2] - RD = √(DX^2 + DY^2) + DX = X[i] - center[1] + DY = Y[i] - center[2] + RD = √(DX^2 + DY^2) # get radial distance from crater rim - RD -= crater + RD -= crater # find position relative to crater rim - dr = radius - crater - pos[i] = -RD / dr + 1 + dr = radius - crater + pos[i] = -RD / dr + 1 end ## assign topography - H = zeros(nx,ny) + H = zeros(nx, ny) # check if there is a background supplied if background === nothing - H .= base + H .= base else # background = nondimensionalize(background, CharUnits) if size(background) == size(X) H .= background - elseif size(background) == size(reshape(X,nx,ny,1)) - H .= @views background[:,:,1] + elseif size(background) == size(reshape(X, nx, ny, 1)) + H .= @views background[:, :, 1] else error("Size of background must be ", nx, "x", ny) end @@ -979,12 +1276,12 @@ Parameters end function compute_thermal_structure(Temp, X, Y, Z, Phase, s::LinearTemp) - @unpack Ttop, Tbot = s + @unpack Ttop, Tbot = s - dz = Z[end]-Z[1]; - dT = Tbot - Ttop + dz = Z[end] - Z[1] + dT = Tbot - Ttop - Temp = abs.(Z./dz).*dT .+ Ttop + Temp = abs.((Z .- Z[end]) ./ dz) .* dT .+ Ttop return Temp end @@ -1004,22 +1301,22 @@ Parameters @with_kw_noshow mutable struct HalfspaceCoolingTemp <: AbstractThermalStructure Tsurface = 0 # top T Tmantle = 1350 # bottom T - Age = 60 # thermal age of plate [in Myrs] + Age = 60 # thermal age of plate [in Myrs] Adiabat = 0 # Adiabatic gradient in K/km end function compute_thermal_structure(Temp, X, Y, Z, Phase, s::HalfspaceCoolingTemp) - @unpack Tsurface, Tmantle, Age, Adiabat = s + @unpack Tsurface, Tmantle, Age, Adiabat = s - kappa = 1e-6; - SecYear = 3600*24*365 - dz = Z[end]-Z[1]; - ThermalAge = Age*1e6*SecYear; + kappa = 1.0e-6 + SecYear = 3600 * 24 * 365 + dz = Z[end] - Z[1] + ThermalAge = Age * 1.0e6 * SecYear - MantleAdiabaticT = Tmantle .+ Adiabat*abs.(Z); # Adiabatic temperature of mantle + MantleAdiabaticT = Tmantle .+ Adiabat * abs.(Z) # Adiabatic temperature of mantle for i in eachindex(Temp) - Temp[i] = (Tsurface .- Tmantle)*erfc((abs.(Z[i])*1e3)./(2*sqrt(kappa*ThermalAge))) + MantleAdiabaticT[i]; + Temp[i] = (Tsurface .- Tmantle) * erfc((abs.(Z[i] - Z[end]) * 1.0e3) ./ (2 * sqrt(kappa * ThermalAge))) + MantleAdiabaticT[i] end return Temp end @@ -1050,55 +1347,158 @@ Note: the thermal age at the mid oceanic ridge is set to 1 year to avoid divisio MORside = "left" # side of box where the MOR is located SpreadingVel = 3 # spreading velocity [cm/yr] AgeRidge = 0 # Age of the ridge [Myrs] - maxAge = 60 # maximum thermal age of plate [Myrs] + maxAge = 60 # maximum thermal age of plate [Myrs] end function compute_thermal_structure(Temp, X, Y, Z, Phase, s::SpreadingRateTemp) - @unpack Tsurface, Tmantle, Adiabat, MORside, SpreadingVel, AgeRidge, maxAge = s - - kappa = 1e-6; - SecYear = 3600*24*365 - dz = Z[end]-Z[1]; - - MantleAdiabaticT = Tmantle .+ Adiabat*abs.(Z); # Adiabatic temperature of mantle - - if MORside=="left" - Distance = X .- X[1,1,1]; - elseif MORside=="right" - Distance = X[end,1,1] .- X; - elseif MORside=="front" - Distance = Y .- Y[1,1,1]; - elseif MORside=="back" - Distance = Y[1,end,1] .- Y; + @unpack Tsurface, Tmantle, Adiabat, MORside, SpreadingVel, AgeRidge, maxAge = s + + kappa = 1.0e-6 + SecYear = 3600 * 24 * 365 + dz = Z[end] - Z[1] + + MantleAdiabaticT = Tmantle .+ Adiabat * abs.(Z) # Adiabatic temperature of mantle + + if MORside == "left" + Distance = X .- X[1, 1, 1] + elseif MORside == "right" + Distance = X[end, 1, 1] .- X + elseif MORside == "front" + Distance = Y .- Y[1, 1, 1] + elseif MORside == "back" + Distance = Y[1, end, 1] .- Y + else error("unknown side") end for i in eachindex(Temp) - ThermalAge = abs(Distance[i]*1e3*1e2)/SpreadingVel + AgeRidge*1e6; # Thermal age in years - if ThermalAge>maxAge*1e6 - ThermalAge = maxAge*1e6 + ThermalAge = abs(Distance[i] * 1.0e3 * 1.0e2) / SpreadingVel + AgeRidge * 1.0e6 # Thermal age in years + if ThermalAge > maxAge * 1.0e6 + ThermalAge = maxAge * 1.0e6 end - ThermalAge = ThermalAge*SecYear; - if ThermalAge==0 - ThermalAge = 1e-6 # doesn't like zero + ThermalAge = ThermalAge * SecYear + if ThermalAge == 0 + ThermalAge = 1.0e-6 # doesn't like zero end - Temp[i] = (Tsurface .- Tmantle)*erfc((abs.(Z[i])*1e3)./(2*sqrt(kappa*ThermalAge))) + MantleAdiabaticT[i]; + Temp[i] = (Tsurface .- Tmantle) * erfc((abs.(Z[i]) * 1.0e3) ./ (2 * sqrt(kappa * ThermalAge))) + MantleAdiabaticT[i] + end + return Temp end """ - LithosphericTemp(Tsurface=0.0, Tpot=1350.0, dTadi=0.5, - ubound="const", lbound="const, utbf = 50.0e-3, ltbf = 10.0e-3, - age = 120.0, dtfac = 0.9, nz = 201, - rheology = example_CLrheology() + SpreadingRateTemp(Temp, X, Y, Z, Phase, s::SpreadingRateTemp, segments) + +Calculates the temperature distribution across the plate considering multiple ridge segments. + +This function computes the thermal structure based on the perpendicular distance from each point to its corresponding ridge segment, and applies a thermal model using a spreading velocity and thermal age. + +Parameters +========== +- Temp : Temperature field to be updated (array) +- X, Y, Z : Coordinates of the points (arrays) +- Phase : Phase of the material (unused in this version) +- s : SpreadingRateTemp object containing the thermal and spreading parameters +- segments : List of ridge segments, where each segment is defined by two tuples representing the start and end coordinates (x1, y1) and (x2, y2) for each segment. + +Note +==== +The temperature at each point is calculated using the thermal age, which is determined by the distance from the point to the nearest ridge segment and the spreading velocity. + +The function works in the context of one or more segments. The key difference from the previous function is that the ridge can now be placed at any position within the box, not just at the boundary. + +The thermal age is capped at `maxAge` years, and the temperature is adjusted based on the distance to the ridge and the corresponding thermal gradient. + +""" + +function compute_thermal_structure(Temp, X, Y, Z, Phase, s::SpreadingRateTemp, segments::Vector{Tuple{Tuple{Float64, Float64}, Tuple{Float64, Float64}}}) + @unpack Tsurface, Tmantle, Adiabat, SpreadingVel, AgeRidge, maxAge = s + kappa = 1.0e-6 + SecYear = 3600 * 24 * 365 + dz = Z[end] - Z[1] + + MantleAdiabaticT = Tmantle .+ Adiabat * abs.(Z) + + #Create delimiters + delimiters = [(segments[i][2], segments[i + 1][1]) for i in 1:(length(segments) - 1)] + + for I in eachindex(X) + px, py, pz = X[I], Y[I], Z[I] + + # Determine region of point + region = determine_region(px, py, delimiters, segments) + + # Select the corresponding segment + x1, y1 = segments[region][1] + x2, y2 = segments[region][2] + + # Calculate distance to segment + Distance = perpendicular_distance_to_segment(px, py, x1, y1, x2, y2) + + # Calculate thermal age + ThermalAge = abs(Distance * 1.0e5) / SpreadingVel + AgeRidge * 1.0e6 # Thermal age in years + if ThermalAge > maxAge * 1.0e6 + ThermalAge = maxAge * 1.0e6 + end + + ThermalAge = ThermalAge * SecYear # Convert to seconds + if ThermalAge == 0 + ThermalAge = 1.0e-6 # Avoid zero + end + + # Calculate temperature + Temp[I] = (Tsurface - Tmantle) * erfc(abs(pz) * 1.0e3 / (2 * sqrt(kappa * ThermalAge))) + MantleAdiabaticT[I] + end + + return Temp + +end + +# Supporting functions for multi-segment ridge functionality + +# Function to calculate the perpendicular distance from a point to a segment +function perpendicular_distance_to_segment(x, y, x1, y1, x2, y2) + num = abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1) + den = sqrt((y2 - y1)^2 + (x2 - x1)^2) + return num / den +end + +# Function to determine the side of a point with respect to a line (adjusted for segment direction) +function side_of_line(x, y, x1, y1, x2, y2, direction) + side = (x2 - x1) * (y - y1) - (y2 - y1) * (x - x1) + return direction == :left ? side > 0 : side < 0 +end + +# Function to determine in which region a point lies (based on delimiters) +function determine_region(px, py, delimiters, segments) + for i in 1:length(delimiters) + x1, y1 = delimiters[i][1] + x2, y2 = delimiters[i][2] + + # Determine the direction of the segments + direction = x2 < x1 ? :left : :right + + # Check the side of the line considering the direction + if side_of_line(px, py, x1, y1, x2, y2, direction) + return i # Region corresponding to segment i + end + end + return length(segments) # Last region +end + +""" + LithosphericTemp(Tsurface=0.0, Tpot=1350.0, dTadi=0.5, + ubound="const", lbound="const, utbf = 50.0e-3, ltbf = 10.0e-3, + age = 120.0, dtfac = 0.9, nz = 201, + rheology = example_CLrheology() ) -Calculates a 1D temperature profile [C] for variable thermal parameters including radiogenic heat source and - linearly interpolates the temperature profile onto the box. The thermal parameters are defined in +Calculates a 1D temperature profile [C] for variable thermal parameters including radiogenic heat source and + linearly interpolates the temperature profile onto the box. The thermal parameters are defined in rheology and the structure of the lithosphere is define by LithosphericPhases(). @@ -1107,7 +1507,7 @@ Parameters - Tsurface : surface temperature [C] - Tpot : potential mantle temperature [C] - dTadi : adiabatic gradient [K/km] -- ubound : Upper thermal boundary condition ["const","flux"] +- ubound : Upper thermal boundary condition ["const","flux"] - lbound : Lower thermal boundary condition ["const","flux"] - utbf : Upper thermal heat flux [W/m]; if ubound == "flux" - ltbf : Lower thermal heat flux [W/m]; if lbound == "flux" @@ -1127,7 +1527,7 @@ Parameters ltbf = 10.0e-3 # q [W/m^2]; if lbound = "flux" age = 120.0 # Lithospheric age [Ma] dtfac = 0.9 # Diffusion stability criterion - nz = 201 + nz = 201 rheology = example_CLrheology() end @@ -1138,173 +1538,173 @@ struct Thermal_parameters{A} ρCp::A H::A function Thermal_parameters(ni) - ρ = zeros(ni) - Cp = zeros(ni) - k = zeros(ni) - ρCp = zeros(ni) - H = zeros(ni) - new{typeof(ρ)}(ρ,Cp,k,ρCp,H) + ρ = zeros(ni) + Cp = zeros(ni) + k = zeros(ni) + ρCp = zeros(ni) + H = zeros(ni) + return new{typeof(ρ)}(ρ, Cp, k, ρCp, H) end end function compute_thermal_structure(Temp, X, Y, Z, Phase, s::LithosphericTemp) - @unpack Tsurface, Tpot, dTadi, ubound, lbound, utbf, ltbf, age, + @unpack Tsurface, Tpot, dTadi, ubound, lbound, utbf, ltbf, age, dtfac, nz, rheology = s # Create 1D depth profile within the box - z = LinRange(round(maximum(Z)),round(minimum(Z)),nz) # [km] - z = @. z*1e3 # [m] - dz = z[2] - z[1] # Gride resolution - + z = LinRange(maximum(Z) * 1.0e3, minimum(Z) * 1.0e3, nz) # [m] + dz = z[2] - z[1] # Gride resolution # Initialize 1D arrays for explicit solver - T = zeros(nz) - phase = Int64.(zeros(nz)) + T = zeros(nz) + phase = Int64.(zeros(nz)) # Assign phase id from Phase to 1D phase array - phaseid = (minimum(Phase):1:maximum(Phase)) - ztop = round(maximum(Z[findall(Phase .== phaseid[1])])) - zlayer = zeros(length(phaseid)) - for i = 1:length(phaseid) + phaseid = (minimum(Phase):1:maximum(Phase)) + zsurf = maximum(Z[findall(Phase .== phaseid[1])]) * 1.0e3 + zbase = zeros(length(phaseid)) # base of each layer + + # for each phase id + for i in 1:length(phaseid) # Calculate layer thickness from Phase array - zlayer[i] = round(minimum(Z[findall(Phase .== phaseid[i])])) - zlayer[i] = zlayer[i]*1.0e3 + zbase[i] = minimum(Z[findall(Phase .== phaseid[i])]) * 1.0e3 end - for i = 1:length(phaseid) + for i in 1:length(phaseid) # Assign phase ids - ind = findall((z .>= zlayer[i]) .& (z .<= ztop)) - phase[ind] .= phaseid[i] - ztop = zlayer[i] + ztop = i === 1 ? zsurf : zbase[i - 1] + ind = findall((z .>= zbase[i]) .& (z .<= ztop)) + phase[ind] .= phaseid[i] end # Setup initial T-profile - Tpot = Tpot + 273.15 # Potential temp [K] - Tsurface = Tsurface + 273.15 # Surface temperature [ K ] - T = @. Tpot + abs.(z./1.0e3)*dTadi # Initial T-profile [ K ] - T[1] = Tsurface - - args = (;) - thermal_parameters = Thermal_parameters(nz) - + Tpot = Tpot + 273.15 # Potential temp [K] + Tsurface = Tsurface + 273.15 # Surface temperature [ K ] + T = @. Tpot + abs.(z ./ 1.0e3) * dTadi # Initial T-profile [ K ] + T[1] = Tsurface + + args = (;) + thermal_parameters = Thermal_parameters(nz) + ## Update thermal parameters ======================================== # - compute_density!(thermal_parameters.ρ,rheology,phase,args) - compute_heatcapacity!(thermal_parameters.Cp,rheology,phase,args) - compute_conductivity!(thermal_parameters.k,rheology,phase,args) - thermal_parameters.ρCp .= @. thermal_parameters.Cp * thermal_parameters.ρ - compute_radioactive_heat!(thermal_parameters.H,rheology,phase,args) + compute_density!(thermal_parameters.ρ, rheology, phase, args) + compute_heatcapacity!(thermal_parameters.Cp, rheology, phase, args) + compute_conductivity!(thermal_parameters.k, rheology, phase, args) + thermal_parameters.ρCp .= @. thermal_parameters.Cp * thermal_parameters.ρ + compute_radioactive_heat!(thermal_parameters.H, rheology, phase, args) # Thermal diffusivity [ m^2/s ] - κ = maximum(thermal_parameters.k) / + κ = maximum(thermal_parameters.k) / minimum(thermal_parameters.ρ) / minimum(thermal_parameters.Cp) ## =================================================================== # ## Time stability criterion ========================================= # - tfac = 60.0*60.0*24.0*365.25 # Seconds per year - age = age*1.0e6*tfac # Age in seconds - dtexp = dz^2.0/2.0/κ # Stability criterion for explicit - dt = dtfac*dtexp # [s] - nit = Int64(ceil(age/dt)) # Number of iterations - time = zeros(nit) # Time array - - for i = 1:nit + tfac = 60.0 * 60.0 * 24.0 * 365.25 # Seconds per year + age = age * 1.0e6 * tfac # Age in seconds + dtexp = dz^2.0 / 2.0 / κ # Stability criterion for explicit + dt = dtfac * dtexp # [s] + nit = Int64(ceil(age / dt)) # Number of iterations + time = zeros(nit) # Time array + + for i in 1:nit if i > 1 - time[i] = time[i-1] + dt + time[i] = time[i - 1] + dt end SolveDiff1Dexplicit_vary!( T, thermal_parameters, - ubound,lbound, - utbf,ltbf, + ubound, lbound, + utbf, ltbf, dz, - dt) + dt + ) end - interp_linear_T = linear_interpolation(-z./1.0e3, T.-273.15) # create interpolation object + interp_linear_T = linear_interpolation(-z ./ 1.0e3, T .- 273.15) # create interpolation object Temp = interp_linear_T(-Z) - + return Temp end -function SolveDiff1Dexplicit_vary!( - T, - thermal_parameters, - ubound,lbound, - utbf,ltbf, - di, - dt -) - nz = length(T) - T0 = T +function SolveDiff1Dexplicit_vary!( + T, + thermal_parameters, + ubound, lbound, + utbf, ltbf, + di, + dt + ) + nz = length(T) + T0 = T if ubound == "const" - T[1] = T0[1] + T[1] = T0[1] elseif ubound == "flux" - kB = (thermal_parameters.k[2] + thermal_parameters.k[1])/2.0 - kA = (thermal_parameters.k[1] + thermal_parameters.k[1])/2.0 - a = (dt*(kA + kB)) / (di^2.0 * thermal_parameters.ρCp[1]) - b = 1 - (dt*(kA + kB)) / (di^2.0 * thermal_parameters.ρCp[1]) - c = (dt*2.0*utbf)/(di * thermal_parameters.ρCp[1]) - T[1] = a*T0[2] + b*T0[1] + c + - thermal_parameters.H[1]*dt/thermal_parameters.ρCp[1] + kB = (thermal_parameters.k[2] + thermal_parameters.k[1]) / 2.0 + kA = (thermal_parameters.k[1] + thermal_parameters.k[1]) / 2.0 + a = (dt * (kA + kB)) / (di^2.0 * thermal_parameters.ρCp[1]) + b = 1 - (dt * (kA + kB)) / (di^2.0 * thermal_parameters.ρCp[1]) + c = (dt * 2.0 * utbf) / (di * thermal_parameters.ρCp[1]) + T[1] = a * T0[2] + b * T0[1] + c + + thermal_parameters.H[1] * dt / thermal_parameters.ρCp[1] end if lbound == "const" - T[nz] = T0[nz] + T[nz] = T0[nz] elseif lbound == "flux" - kB = (thermal_parameters.k[nz] + thermal_parameters.k[nz])/2.0 - kA = (thermal_parameters.k[nz] + thermal_parameters.k[nz-1])/2.0 - a = (dt*(kA + kB)) / (di^2.0 * thermal_parameters.ρCp[nz]) - b = 1 - (dt*(kA + kB)) / (di^2.0 * thermal_parameters.ρCp[nz]) - c = -(dt*2.0*ltbf) / (di * thermal_parameters.ρCp[nz]) - T[nz] = a*T0[nz-1] + b*T0[nz] + c + kB = (thermal_parameters.k[nz] + thermal_parameters.k[nz]) / 2.0 + kA = (thermal_parameters.k[nz] + thermal_parameters.k[nz - 1]) / 2.0 + a = (dt * (kA + kB)) / (di^2.0 * thermal_parameters.ρCp[nz]) + b = 1 - (dt * (kA + kB)) / (di^2.0 * thermal_parameters.ρCp[nz]) + c = -(dt * 2.0 * ltbf) / (di * thermal_parameters.ρCp[nz]) + T[nz] = a * T0[nz - 1] + b * T0[nz] + c end - kAi = @. (thermal_parameters.k[1:end-2] + thermal_parameters.k[2:end-1])/2.0 - kBi = @. (thermal_parameters.k[2:end-1] + thermal_parameters.k[3:end])/2.0 - ai = @. (kBi*dt)/(di^2.0*thermal_parameters.ρCp[2:end-1]) - bi = @. 1.0 - (dt*(kAi + kBi))/(di^2.0*thermal_parameters.ρCp[2:end-1]) - ci = @. (kAi*dt)/(di^2.0*thermal_parameters.ρCp[2:end-1]) - T[2:end-1] = @. ai*T0[3:end] + bi*T0[2:end-1] + ci*T0[1:end-2] + - thermal_parameters.H[2:end-1]*dt/thermal_parameters.ρCp[2:end-1] - return T + kAi = @. (thermal_parameters.k[1:(end - 2)] + thermal_parameters.k[2:(end - 1)]) / 2.0 + kBi = @. (thermal_parameters.k[2:(end - 1)] + thermal_parameters.k[3:end]) / 2.0 + ai = @. (kBi * dt) / (di^2.0 * thermal_parameters.ρCp[2:(end - 1)]) + bi = @. 1.0 - (dt * (kAi + kBi)) / (di^2.0 * thermal_parameters.ρCp[2:(end - 1)]) + ci = @. (kAi * dt) / (di^2.0 * thermal_parameters.ρCp[2:(end - 1)]) + T[2:(end - 1)] = @. ai * T0[3:end] + bi * T0[2:(end - 1)] + ci * T0[1:(end - 2)] + + thermal_parameters.H[2:(end - 1)] * dt / thermal_parameters.ρCp[2:(end - 1)] + return T end -function example_CLrheology(; - ρM=3.0e3, # Density [ kg/m^3 ] - CpM=1.0e3, # Specific heat capacity [ J/kg/K ] - kM=2.3, # Thermal conductivity [ W/m/K ] - HM=0.0, # Radiogenic heat source per mass [H] = W/kg; [H] = [Q/rho] - ρUC=2.7e3, # Density [ kg/m^3 ] - CpUC=1.0e3, # Specific heat capacity [ J/kg/K ] - kUC=3.0, # Thermal conductivity [ W/m/K ] - HUC=617.0e-12, # Radiogenic heat source per mass [H] = W/kg; [H] = [Q/rho] - ρLC=2.9e3, # Density [ kg/m^3 ] - CpLC=1.0e3, # Specific heat capacity [ J/kg/K ] - kLC=2.0, # Thermal conductivity [ W/m/K ] - HLC=43.0e-12, # Radiogenic heat source per mass [H] = W/kg; [H] = [Q/rho] -) +function example_CLrheology(; + ρM = 3.0e3, # Density [ kg/m^3 ] + CpM = 1.0e3, # Specific heat capacity [ J/kg/K ] + kM = 2.3, # Thermal conductivity [ W/m/K ] + HM = 0.0, # Radiogenic heat source per mass [H] = W/kg; [H] = [Q/rho] + ρUC = 2.7e3, # Density [ kg/m^3 ] + CpUC = 1.0e3, # Specific heat capacity [ J/kg/K ] + kUC = 3.0, # Thermal conductivity [ W/m/K ] + HUC = 617.0e-12, # Radiogenic heat source per mass [H] = W/kg; [H] = [Q/rho] + ρLC = 2.9e3, # Density [ kg/m^3 ] + CpLC = 1.0e3, # Specific heat capacity [ J/kg/K ] + kLC = 2.0, # Thermal conductivity [ W/m/K ] + HLC = 43.0e-12, # Radiogenic heat source per mass [H] = W/kg; [H] = [Q/rho] + ) rheology = ( # Name = "UpperCrust", SetMaterialParams(; - Phase = 1, - Density = ConstantDensity(; ρ=ρUC), - HeatCapacity = ConstantHeatCapacity(; Cp=CpUC), - Conductivity = ConstantConductivity(; k=kUC), - RadioactiveHeat = ConstantRadioactiveHeat(; H_r=HUC*ρUC), # [H] = W/m^3 + Phase = 1, + Density = ConstantDensity(; ρ = ρUC), + HeatCapacity = ConstantHeatCapacity(; Cp = CpUC), + Conductivity = ConstantConductivity(; k = kUC), + RadioactiveHeat = ConstantRadioactiveHeat(; H_r = HUC * ρUC), # [H] = W/m^3 ), # Name = "LowerCrust", SetMaterialParams(; - Phase = 2, - Density = ConstantDensity(; ρ=ρLC), - HeatCapacity = ConstantHeatCapacity(; Cp=CpLC), - Conductivity = ConstantConductivity(; k=kLC), - RadioactiveHeat = ConstantRadioactiveHeat(; H_r=HLC*ρLC), # [H] = W/m^3 + Phase = 2, + Density = ConstantDensity(; ρ = ρLC), + HeatCapacity = ConstantHeatCapacity(; Cp = CpLC), + Conductivity = ConstantConductivity(; k = kLC), + RadioactiveHeat = ConstantRadioactiveHeat(; H_r = HLC * ρLC), # [H] = W/m^3 ), # Name = "LithosphericMantle", SetMaterialParams(; - Phase = 3, - Density = ConstantDensity(; ρ=ρM), - HeatCapacity = ConstantHeatCapacity(; Cp=CpM), - Conductivity = ConstantConductivity(; k=kM), - RadioactiveHeat = ConstantRadioactiveHeat(; H_r=HM*ρM), # [H] = W/m^3 + Phase = 3, + Density = ConstantDensity(; ρ = ρM), + HeatCapacity = ConstantHeatCapacity(; Cp = CpM), + Conductivity = ConstantConductivity(; k = kM), + RadioactiveHeat = ConstantRadioactiveHeat(; H_r = HM * ρM), # [H] = W/m^3 ), ) return rheology @@ -1332,7 +1732,6 @@ function compute_phase(Phase, Temp, X, Y, Z, s::ConstantPhase) end - """ LithosphericPhases(Layers=[10 20 15], Phases=[1 2 3 4], Tlab=nothing ) @@ -1346,9 +1745,9 @@ Parameters """ @with_kw_noshow mutable struct LithosphericPhases <: AbstractPhaseNumber - Layers = [10., 20., 15.] - Phases = [1, 2 , 3, 4] - Tlab = nothing + Layers = [10.0, 20.0, 15.0] + Phases = [1, 2, 3, 4] + Tlab = nothing end @@ -1372,22 +1771,22 @@ Parameters - Ztop - Vertical coordinate of top of model box - Grid - Grid structure (usually obtained with read_LaMEM_inputfile) """ -function compute_phase(Phase, Temp, X, Y, Z, s::LithosphericPhases; Ztop=0) - @unpack Layers, Phases, Tlab = s +function compute_phase(Phase, Temp, X, Y, Z, s::LithosphericPhases; Ztop = 0) + @unpack Layers, Phases, Tlab = s Phase .= Phases[end] - for i = 1 : length(Layers) - Zbot = Ztop-Layers[i] - ind = findall( ( Z .>= Zbot) .& (Z .<= Ztop) ); + for i in 1:length(Layers) + Zbot = Ztop - Layers[i] + ind = findall((Z .>= Zbot) .& (Z .<= Ztop)) Phase[ind] .= Phases[i] - Ztop = Zbot + Ztop = Zbot end # set phase to mantle if requested if Tlab != nothing - ind = findall(Temp .> Tlab) + ind = findall(Temp .> Tlab) Phase[ind] .= Phases[end] end @@ -1395,7 +1794,7 @@ function compute_phase(Phase, Temp, X, Y, Z, s::LithosphericPhases; Ztop=0) end # allow AbstractGeneralGrid instead of Z and Ztop -compute_phase(Phase, Temp, Grid::LaMEM_grid, s::LithosphericPhases) = compute_phase(Phase, Temp, Grid.X, Grid.Y, Grid.Z, s::LithosphericPhases, Ztop=maximum(Grid.coord_z)) +compute_phase(Phase, Temp, Grid::LaMEM_grid, s::LithosphericPhases) = compute_phase(Phase, Temp, Grid.X, Grid.Y, Grid.Z, s::LithosphericPhases, Ztop = maximum(Grid.coord_z)) """ @@ -1405,69 +1804,69 @@ Thermal structure by McKenzie for a subducted slab that is fully embedded in the Parameters === -- Tsurface: Top T [C] -- Tmantle: Bottom T [C] -- Adiabat: Adiabatic gradient in K/km -- v_cm_yr: Subduction velocity [cm/yr] -- κ: Thermal diffusivity [m2/s] -- it: Number iterations employed in the harmonic summation +- `Tsurface`: Top T [C] +- `Tmantle`: Bottom T [C] +- `Adiabat`: Adiabatic gradient in K/km +- `v_cm_yr`: Subduction velocity [cm/yr] +- `κ`: Thermal diffusivity [m2/s] +- `it`: Number iterations employed in the harmonic summation """ @with_kw_noshow mutable struct McKenzie_subducting_slab <: AbstractThermalStructure Tsurface::Float64 = 20.0 # top T - Tmantle::Float64 = 1350.0 # bottom T - Adiabat::Float64 = 0.4 # Adiabatic gradient in K/km - v_cm_yr::Float64 = 2.0 # velocity of subduction [cm/yr] - κ::Float64 = 1e-6 # Thermal diffusivity [m2/s] - it::Int64 = 36 # number of harmonic summation (look Mckenzie formula) + Tmantle::Float64 = 1350.0 # bottom T + Adiabat::Float64 = 0.4 # Adiabatic gradient in K/km + v_cm_yr::Float64 = 2.0 # velocity of subduction [cm/yr] + κ::Float64 = 1.0e-6 # Thermal diffusivity [m2/s] + it::Int64 = 36 # number of harmonic summation (look Mckenzie formula) end -""" +""" compute_thermal_structure(Temp, X, Y, Z, Phase, s::McKenzie_subducting_slab) Compute the temperature field of a `McKenzie_subducting_slab`. Uses the analytical solution of McKenzie (1969) ["Speculations on the consequences and causes of plate motions"]. The functions assumes -that the bottom of the slab is the coordinate Z=0. Internally the function shifts the coordinate. +that the bottom of the slab is the coordinate Z=0. Internally the function shifts the coordinate. Parameters ============================= -Temp Temperature array -- `X` X Array -- `Y` Y Array -- `Z` Z Array -- `Phase` Phase array -- `s` McKenzie_subducting_slab -""" -function compute_thermal_structure(Temp, X, Y, Z,Phase, s::McKenzie_subducting_slab) +- `Temp`: Temperature array +- `X`: X Array +- `Y`: Y Array +- `Z`: Z Array +- `Phase`: Phase array +- `s`: `McKenzie_subducting_slab` +""" +function compute_thermal_structure(Temp, X, Y, Z, Phase, s::McKenzie_subducting_slab) @unpack Tsurface, Tmantle, Adiabat, v_cm_yr, κ, it = s - # Thickness of the layer: - Thickness = (maximum(Z)-minimum(Z)); - Zshift = Z .- Z[end] # McKenzie model is defined with Z = 0 at the bottom of the slab + # Thickness of the layer: + Thickness = (maximum(Z) - minimum(Z)) + Zshift = Z .- Z[end] # McKenzie model is defined with Z = 0 at the bottom of the slab + + # Convert subduction velocity from cm/yr -> m/s; + convert_velocity = 1 / (100.0 * 365.25 * 60.0 * 60.0 * 24.0) + v_s = v_cm_yr * convert_velocity - # Convert subduction velocity from cm/yr -> m/s; - convert_velocity = 1/(100.0*365.25*60.0*60.0*24.0); - v_s = v_cm_yr*convert_velocity; - # calculate the thermal Reynolds number - Re = (v_s*Thickness*1000)/2/κ; # factor 1000 to transfer Thickness from km to m - + Re = (v_s * Thickness * 1000) / 2 / κ # factor 1000 to transfer Thickness from km to m + # McKenzie model - sc = 1/Thickness - σ = ones(size(Temp)); + sc = 1 / Thickness + σ = ones(size(Temp)) # Dividi et impera - for i=1:it - a = (-1.0).^(i)./(i.*pi) - b = (Re .- (Re.^2 .+ i^2.0 .* pi^2.0).^(0.5)) .*X .*sc; - c = sin.(i .*pi .*(1 .- abs.(Zshift .*sc))) ; - e = exp.(b); - σ .+= 2*a.*e.*c + for i in 1:it + a = (-1.0) .^ (i) ./ (i .* pi) + b = (Re .- (Re .^ 2 .+ i^2.0 .* pi^2.0) .^ (0.5)) .* X .* sc + c = sin.(i .* pi .* (1 .- abs.(Zshift .* sc))) + e = exp.(b) + σ .+= 2 * a .* e .* c end - Temp .= Tsurface .+ (Tmantle-Tsurface).*σ; - Temp .= Temp + (Adiabat*abs.(Z)) - + Temp .= Tsurface .+ (Tmantle - Tsurface) .* σ + Temp .= Temp + (Adiabat * abs.(Z)) + return Temp end @@ -1486,59 +1885,63 @@ Parameters - F2: Second temperature field """ -@with_kw_noshow mutable struct LinearWeightedTemperature <: AbstractThermalStructure - w_min::Float64 = 0.0; - w_max::Float64 = 1.0; - crit_dist::Float64 = 100.0; - dir::Symbol =:X; - F1::AbstractThermalStructure = ConstantTemp(); - F2::AbstractThermalStructure = ConstantTemp(); +@with_kw_noshow mutable struct LinearWeightedTemperature <: AbstractThermalStructure + w_min::Float64 = 0.0 + w_max::Float64 = 1.0 + crit_dist::Float64 = 100.0 + dir::Symbol = :X + F1::AbstractThermalStructure = ConstantTemp() + F2::AbstractThermalStructure = ConstantTemp() end """ compute_thermal_structure(Temp, X, Y, Z, Phase, s::LinearWeightedTemperature) - + Weight average along distance -Do a weight average between two field along a specified direction -Given a distance {could be any array, from X,Y} -> it increase from the origin the weight of -F1, while F2 decreases. -This function has been conceived for averaging the solution of Mckenzie and half space cooling model, but in -can be used to smooth the temperature field from continent ocean: --> Select the boundary to apply; --> transform the coordinate such that dist represent the perpendicular direction along which you want to apply -this smoothening and in a such way that 0.0 is the point in which the weight of F1 is equal to 0.0; --> Select the points that belongs to this area -> compute the thermal fields {F1} {F2} -> then modify F. + +Do a weight average between two field along a specified direction + +Given a distance (could be any array, from X,Y) -> the weight of F1 increase from the origin, while F2 decreases. + +This function has been conceived for averaging the solution of McKenzie and half space cooling models, but it +can be used to smooth the temperature field from continent ocean: +- Select the boundary to apply; +- transform the coordinate such that dist represent the perpendicular direction along which you want to apply this smoothening + and in a such way that 0.0 is the point in which the weight of F1 is equal to 0.0; +- Select the points that belongs to this area +- compute the thermal fields {F1} {F2} +- then modify F. """ function compute_thermal_structure(Temp, X, Y, Z, Phase, s::LinearWeightedTemperature) - @unpack w_min, w_max, crit_dist,dir = s; - @unpack F1, F2 = s; - + @unpack w_min, w_max, crit_dist, dir = s + @unpack F1, F2 = s + if dir === :X - dist = X; - elseif dir ===:Y - dist = Y; + dist = X + elseif dir === :Y + dist = Y else - dist = Z; + dist = Z end - + # compute the 1D thermal structures - Temp1 = zeros(size(Temp)); - Temp2 = zeros(size(Temp)); - Temp1 = compute_thermal_structure(Temp1, X, Y, Z, Phase, F1); - Temp2 = compute_thermal_structure(Temp2, X, Y, Z, Phase, F2); + Temp1 = zeros(size(Temp)) + Temp2 = zeros(size(Temp)) + Temp1 = compute_thermal_structure(Temp1, X, Y, Z, Phase, F1) + Temp2 = compute_thermal_structure(Temp2, X, Y, Z, Phase, F2) # Compute the weights - weight = w_min .+(w_max-w_min) ./(crit_dist) .*(dist) + weight = w_min .+ (w_max - w_min) ./ (crit_dist) .* (dist) + + ind_1 = findall(weight .> w_max) + ind_2 = findall(weight .< w_min) - ind_1 = findall(weight .>w_max); - ind_2 = findall(weight .0 - println(io," Weakzone phase : $(g.WeakzonePhase)") + println(io, " Depth [d_decoupling]: $(g.d_decoupling) km") + println(io, " Bending [type_bending]: $(g.type_bending)") + println(io, " [WeakzoneThickness] : $(g.WeakzoneThickness) km") + if g.WeakzoneThickness > 0 + println(io, " Weakzone phase : $(g.WeakzonePhase)") end - + return nothing end """ Top, Bot = compute_slab_surface(trench::Trench) -Computes the (`x`,`z`) coordinates of the slab top, bottom surface using the mid surface of the slab as reference. +Computes the (`x`,`z`) coordinates of the slab top, bottom surface using the mid surface of the slab as reference. Parameters -=== +=== - `trench` - `Trench` structure that contains the relevant parameters Method === -It computes it by discretizing the slab surface in `n_seg` segments, and computing the average bending angle (which is a function of the current length of the slab). +It computes it by discretizing the slab surface in `n_seg` segments, and computing the average bending angle (which is a function of the current length of the slab). Next, it compute the coordinates assuming that the trench is at 0.0, and assuming a positive `θ_max` angle. """ function compute_slab_surface(trench::Trench) - @unpack Thickness, Length, n_seg, Lb, θ_max, type_bending, direction, WeakzoneThickness = trench; + @unpack Thickness, Length, n_seg, Lb, θ_max, type_bending, direction, WeakzoneThickness = trench # Convert θ_max into radians - θ_max *= π / 180; + θ_max *= π / 180 # Allocate the top, mid and bottom surface - Top = zeros(n_seg+1,2); - Bottom = zeros(n_seg+1,2); - WeakZone = zeros(n_seg+1,2); - Bottom[1,2] = -Thickness; - WeakZone[1,2] = WeakzoneThickness; - MidS = zeros(n_seg+1,2); - MidS[1,2] = -Thickness/2; + Top = zeros(n_seg + 1, 2) + Bottom = zeros(n_seg + 1, 2) + WeakZone = zeros(n_seg + 1, 2) + Bottom[1, 2] = -Thickness + WeakZone[1, 2] = WeakzoneThickness + MidS = zeros(n_seg + 1, 2) + MidS[1, 2] = -Thickness / 2 # Initialize the length. - l = 0.0; # initial length - it = 1; # iteration + l = 0.0 # initial length + it = 1 # iteration - dl = Length/n_seg; # dl - while lLb +function compute_bending_angle(θ_max::Float64, Lb::Float64, l::Float64, type::Symbol) + + if l > Lb θ = θ_max elseif type === :Ribe # Compute theta - θ = θ_max*l^2*((3*Lb-2*l))/(Lb^3); + θ = θ_max * l^2 * ((3 * Lb - 2 * l)) / (Lb^3) elseif type === :Linear # Compute the actual angle - θ = l*(θ_max-0)/(Lb); + θ = l * (θ_max - 0) / (Lb) end return θ end @@ -1724,64 +2127,65 @@ end Function that finds the perpendicular distance to the top and bottom of the slab `d`, and the current length of the slab `l`. """ -function find_slab_distance!(ls, d, X,Y,Z, Top, Bottom, trench::Trench) - @unpack Thickness, Length, n_seg, Start, End, direction = trench; +function find_slab_distance!(ls, d, X, Y, Z, Top, Bottom, trench::Trench) + @unpack Thickness, Length, n_seg, Start, End, direction = trench # Perform rotation of 3D coordinates along the angle from Start -> End: - Xrot = X .- Start[1]; - Yrot = Y .- Start[2]; - - StrikeAngle = -atand((End[2]-Start[2])/(End[1]-Start[1])) - Rot3D!(Xrot,Yrot,Z, StrikeAngle, 0.0) - - xb = Rot3D(End[1]-Start[1],End[2]-Start[2], 0.0, cosd(StrikeAngle), sind(StrikeAngle), 1.0, 0.0) - + Xrot = X .- Start[1] + Yrot = Y .- Start[2] + + StrikeAngle = -atand((End[2] - Start[2]) / (End[1] - Start[1])) + Rot3D!(Xrot, Yrot, Z, StrikeAngle, 0.0) + + xb = Rot3D(End[1] - Start[1], End[2] - Start[2], 0.0, cosd(StrikeAngle), sind(StrikeAngle), 1.0, 0.0) + # dl - dl = trench.Length/n_seg; + dl = trench.Length / n_seg l = 0 # length at the trench position - D = @SVector [Top[1,2], Bottom[1,2], Bottom[1,2],Top[1,2] ] + D = @SVector [Top[1, 2], Bottom[1, 2], Bottom[1, 2], Top[1, 2]] # Construct the slab - for i = 1:(n_seg-1) - ln = l+dl; + for i in 1:(n_seg - 1) + ln = l + dl - pa = (Top[i,1], Top[i,2]); # D = 0 | L = l - pb = (Bottom[i,1], Bottom[i,2]); # D = -Thickness | L=l + pa = (Top[i, 1], Top[i, 2]) # D = 0 | L = l + pb = (Bottom[i, 1], Bottom[i, 2]) # D = -Thickness | L=l - pc = (Bottom[i+1,1],Bottom[i+1,2]); # D = -Thickness |L=L+dl - pd = (Top[i+1,1],Top[i+1,2]) # D = 0| L = L+dl + pc = (Bottom[i + 1, 1], Bottom[i + 1, 2]) # D = -Thickness |L=L+dl + pd = (Top[i + 1, 1], Top[i + 1, 2]) # D = 0| L = L+dl # Create the polygon - poly_y = @SVector [pa[1],pb[1],pc[1],pd[1]]; - poly_z = @SVector [pa[2],pb[2],pc[2],pd[2]]; + poly_y = @SVector [pa[1], pb[1], pc[1], pd[1]] + poly_z = @SVector [pa[2], pb[2], pc[2], pd[2]] # find a sub set of particles - ymin,ymax = extrema(poly_y); - zmin,zmax = extrema(poly_z); + ymin, ymax = extrema(poly_y) + zmin, zmax = extrema(poly_z) - ind_s = findall(0.0.<= Xrot.<= xb[1] .&& ymin .<= Yrot .<= ymax .&& zmin .<= Z .<= zmax); + ind_s = findall(0.0 .<= Xrot .<= xb[1] .&& ymin .<= Yrot .<= ymax .&& zmin .<= Z .<= zmax) # Find the particles - yp = Yrot[ind_s]; - zp = Z[ind_s]; + yp = Yrot[ind_s] + zp = Z[ind_s] # Initialize the ind that are going to be used by inpoly - ind = zeros(Bool,size(zp)); - inpolygon!(ind,poly_y,poly_z,yp,zp); # determine whether points are inside the polygon or not + ind = zeros(Bool, size(zp)) + inpolygon!(ind, poly_y, poly_z, yp, zp) # determine whether points are inside the polygon or not # indexes of the segment ind_seg = ind_s[ind] # Loop over the chosen particles and interpolate the current value of L and D. for ip in ind_seg - point_ = (Yrot[ip], Z[ip]); - d[ip] = -distance_to_linesegment(point_, pa, pd) - ls[ip] = distance_to_linesegment(point_, pb, pa) + l + point_ = (Yrot[ip], Z[ip]) + d[ip] = -distance_to_linesegment(point_, pa, pd) + ls[ip] = distance_to_linesegment(point_, pb, pa) + l end - + #Update l - l = ln; + l = ln end + return end @@ -1790,39 +2194,39 @@ end Computes the distance normal distance from a point `p` to a line segment defined by the points `v` and `w`. """ -function distance_to_linesegment(p::NTuple{2,_T}, v::NTuple{2,_T}, w::NTuple{2,_T}) where _T<:Number +function distance_to_linesegment(p::NTuple{2, _T}, v::NTuple{2, _T}, w::NTuple{2, _T}) where {_T <: Number} dx = w[1] - v[1] dy = w[2] - v[2] - l2 = dx*dx + dy*dy # i.e. |w-v|^2 - avoid a sqrt + l2 = dx * dx + dy * dy # i.e. |w-v|^2 - avoid a sqrt if l2 == 0.0 dx = p[1] - v[1] dy = p[2] - v[2] - return sqrt(dx*dx + dy*dy) # v == w case + return sqrt(dx * dx + dy * dy) # v == w case end # Consider the line extending the segment, parameterized as v + t (w - v). - # We find projection of point p onto the line. + # We find projection of point p onto the line. # It falls where t = [(p-v) . (w-v)] / |w-v|^2 - t = ((p[1] - v[1])*dx + (p[2] - v[2])*dy) / l2 + t = ((p[1] - v[1]) * dx + (p[2] - v[2]) * dy) / l2 if t < 0.0 dx = p[1] - v[1] dy = p[2] - v[2] - return sqrt(dx*dx + dy*dy) # Beyond the 'v' end of the segment + return sqrt(dx * dx + dy * dy) # Beyond the 'v' end of the segment elseif t > 1.0 dx = p[1] - w[1] dy = p[2] - w[2] - return sqrt(dx*dx + dy*dy) # Beyond the 'w' end of the segment + return sqrt(dx * dx + dy * dy) # Beyond the 'w' end of the segment end projection_x = v[1] + t * dx projection_y = v[2] + t * dy dx = p[1] - projection_x dy = p[2] - projection_y - return sqrt(dx*dx + dy*dy) + return sqrt(dx * dx + dy * dy) end """ add_slab!(Phase, Temp, Grid::AbstractGeneralGrid, trench::Trench; phase = ConstantPhase(1), T = nothing, cell=false) -Adds a curved slab with phase & temperature structure to a 3D model setup. +Adds a curved slab with phase & temperature structure to a 3D model setup. Parameters ==== @@ -1838,7 +2242,7 @@ Examples ======== Example 1) Slab -```julia +```julia-repl julia> x = LinRange(0.0,1200.0,128); julia> y = LinRange(0.0,1200.0,128); julia> z = LinRange(-660,50,128); @@ -1853,48 +2257,154 @@ julia> add_slab!(Phase, Temp, Cart, trench, phase = phase, T = TsHC) ``` """ -function add_slab!(Phase, Temp, Grid::AbstractGeneralGrid, trench::Trench; # required input +function add_slab!( + Phase, Temp, Grid::AbstractGeneralGrid, trench::Trench; # required input phase::AbstractPhaseNumber = ConstantPhase(1), # Sets the phase number(s) in the slab - T::Union{AbstractThermalStructure,Nothing} = nothing, cell=false ) # Sets the thermal structure (various functions are available), - + T::Union{AbstractThermalStructure, Nothing} = nothing, cell = false + ) # Sets the thermal structure (various functions are available), + # Retrieve 3D data arrays for the grid - X,Y,Z = coordinate_grids(Grid, cell=cell) + X, Y, Z = coordinate_grids(Grid, cell = cell) # Compute top and bottom of the slab - Top,Bottom, WeakZone = compute_slab_surface(trench); - + Top, Bottom, WeakZone = compute_slab_surface(trench) + # Find the distance to the slab (along & perpendicular) - d = fill(NaN,size(Grid)); # -> d = distance perpendicular to the slab - ls = fill(NaN,size(Grid)); # -> l = length from the trench along the slab - find_slab_distance!(ls, d, X,Y,Z, Top, Bottom, trench); - - # Function to fill up the temperature and the phase. - ind = findall((-trench.Thickness .<= d .<= 0.0)); - - if isa(T, LinearWeightedTemperature) - l_decouplingind = findall(Top[:,2].<=-trench.d_decoupling); - if !isempty(l_decouplingind) - l_decoupling = Top[l_decouplingind[1],1]; - T.crit_dist = abs(l_decoupling); + d = fill(NaN, size(Grid)) # -> d = distance perpendicular to the slab + ls = fill(NaN, size(Grid)) # -> l = length from the trench along the slab + find_slab_distance!(ls, d, X, Y, Z, Top, Bottom, trench) + + # Function to fill up the temperature and the phase. + ind = findall((-trench.Thickness .<= d .<= 0.0)) + + if !isempty(ind) + if isa(T, LinearWeightedTemperature) + l_decouplingind = findall(Top[:, 2] .<= -trench.d_decoupling) + if !isempty(l_decouplingind) + l_decoupling = Top[l_decouplingind[1], 1] + T.crit_dist = abs(l_decoupling) + end + end + + # Compute thermal structure accordingly. See routines below for different options {Future: introducing the length along the trench for having lateral varying properties along the trench} + if !isnothing(T) + Temp[ind] = compute_thermal_structure(Temp[ind], ls[ind], Y[ind], d[ind], Phase[ind], T) + end + + # Set the phase + Phase[ind] = compute_phase(Phase[ind], Temp[ind], ls[ind], Y[ind], d[ind], phase) + + # Add a weak zone on top of the slab (indicated by a phase number but not by temperature) + if trench.WeakzoneThickness > 0.0 + d_weakzone = fill(NaN, size(Grid)) # -> d = distance perpendicular to the slab + ls_weakzone = fill(NaN, size(Grid)) # -> l = length from the trench along the slab + find_slab_distance!(ls_weakzone, d_weakzone, X, Y, Z, WeakZone, Top, trench) + + ind = findall((-trench.WeakzoneThickness .<= d_weakzone .<= 0.0) .& (Z .> -trench.d_decoupling)) + Phase[ind] .= trench.WeakzonePhase + end + end + + return nothing +end + +""" + add_fault!(Phase, Temp, Grid::AbstractGeneralGrid; + Start=(20,100), End=(10,80), + Fault_thickness=10.0, + Depth_extent=nothing, + DipAngle=0e0, + phase=ConstantPhase(1), + T=nothing, + cell=false) + +Adds a fault to the given 3D grid by modifying the `Phase` and `Temp` arrays. +For a 2D grid, use `add_box` instead. + +# Arguments +- `Phase`: Phase array +- `Temp`: Temp array +- `Grid`: The grid on which the fault is to be added. +- `Start`: Tuple representing the starting coordinates of the fault (X, Y). +- `End`: Tuple representing the ending coordinates of the fault (X, Y). +- `Fault_thickness`: Thickness of the fault. +- `Depth_extent`: Depth extent of the fault. If `nothing`, the fault extends through the entire domain. +- `DipAngle`: Dip angle of the fault. +- `phase`: Phase to be assigned to the fault. +- `T`: Temperature to be assigned to the fault. If `nothing`, the temperature is not modified. + + +# Example +```julia +add_fault!(Phase, Temp, Grid; + Start=(20,100), End=(10,80), + Fault_thickness=10.0, + Depth_extent=(-25.0, 0.0), + DipAngle=-10.0, + phase=ConstantPhase(1) + ) +``` +""" +function add_fault!( + Phase, + Temp, + Grid::AbstractGeneralGrid; + Start = (20, 100), + End = (10, 80), + Fault_thickness = 10.0, + Depth_extent = nothing, + DipAngle = 0.0e0, + phase = ConstantPhase(1), + T = nothing, + cell = false + ) + + # Extract the coordinates + X, Y, Z = coordinate_grids(Grid, cell = cell) + + # Calculate the direction vector from Start to End + direction = (End[1] - Start[1], End[2] - Start[2]) + length = sqrt(direction[1]^2 + direction[2]^2) + unit_direction = (direction[1], direction[2]) ./ length + + # Calculate the fault region based on fault thickness and length + fault_half_thickness = Fault_thickness / 2 + + # Create a mask for the fault region + fault_mask = falses(size(X)) + + for k in 1:size(Z, 3), j in 1:size(Y, 2), i in 1:size(X, 1) + # Rotate the point using the dip angle + x_rot, y_rot, z_rot = Rot3D(X[i, j, k], Y[i, j, k], Z[i, j, k], 1.0, 0.0, cosd(DipAngle), sind(DipAngle)) + + # Calculate the projection of the rotated point onto the fault line + projection_length = (x_rot - Start[1]) * unit_direction[1] + (y_rot - Start[2]) * unit_direction[2] + if 0 ≤ projection_length ≤ length + # Calculate the perpendicular distance to the fault line + perpendicular_distance = abs((x_rot - Start[1]) * unit_direction[2] - (y_rot - Start[2]) * unit_direction[1]) + if perpendicular_distance ≤ fault_half_thickness + fault_mask[i, j, k] = true + end end end - # Compute thermal structure accordingly. See routines below for different options {Future: introducing the length along the trench for having lateral varying properties along the trench} - if !isnothing(T) - Temp[ind] = compute_thermal_structure(Temp[ind], ls[ind], Y[ind], d[ind], Phase[ind], T); + ind = findall(fault_mask) + + # Apply depth extent if provided + if !isnothing(Depth_extent) + ind = ind[Z[ind] .≥ Depth_extent[1] .&& Z[ind] .≤ Depth_extent[2]] end - # Set the phase - Phase[ind] = compute_phase(Phase[ind], Temp[ind], ls[ind], Y[ind], d[ind], phase) + ind_flat = flatten_index_dimensions(Phase, ind) - # Add a weak zone on top of the slab (indicated by a phase number but not by temperature) - if trench.WeakzoneThickness>0.0 - d_weakzone = fill(NaN,size(Grid)); # -> d = distance perpendicular to the slab - ls_weakzone = fill(NaN,size(Grid)); # -> l = length from the trench along the slab - find_slab_distance!(ls_weakzone, d_weakzone, X,Y,Z, WeakZone, Top, trench); + if !isempty(ind_flat) + # Compute thermal structure accordingly + if T != nothing + Temp[ind_flat] = compute_thermal_structure(Temp[ind_flat], X[ind], Y[ind], Z[ind], Phase[ind_flat], T) + end - ind = findall( (-trench.WeakzoneThickness .<= d_weakzone .<= 0.0) .& (Z .>-trench.d_decoupling) ); - Phase[ind] .= trench.WeakzonePhase + # Set the phase + Phase[ind_flat] = compute_phase(Phase[ind_flat], Temp[ind_flat], X[ind], Y[ind], Z[ind], phase) end return nothing diff --git a/test/runtests.jl b/test/runtests.jl index 8125c32f9..a0384bcf8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,89 +1,25 @@ using GeophysicalModelGenerator -using Test +using ParallelTestRunner -@testset verbose = true "GeophysicalModelGenerator" begin - @testset "Data import.jl" begin - include("test_data_import.jl") - end - @testset "Data types.jl" begin - include("test_data_types.jl") - end - @testset "Paraview" begin - include("test_paraview.jl") - end - @testset "Paraview collection" begin - include("test_paraview_collection.jl") - end - @testset "Gravity model" begin - include("test_voxel_gravity.jl") - end - @testset "Nearest points" begin - include("test_nearest_points.jl") - end - @testset "Utils" begin - include("test_utils.jl") - end - @testset "Transformations" begin - include("test_transformation.jl") - end - @testset "Surfaces" begin - include("test_surfaces.jl") - end - - @testset "pTatin" begin - include("test_pTatin_IO.jl") - end - - @testset "SetupGeometry" begin - include("test_setup_geometry.jl") - end - - @testset "STL" begin - include("test_stl.jl") - end - - @testset "IO" begin - include("test_IO.jl") - end +testsuite = find_tests(@__DIR__) - @testset "ProfileProcessing" begin - include("test_ProfileProcessing.jl") +# Add `using GeophysicalModelGenerator` to each test +for (name, expr) in testsuite + if name != "test_tutorials" # Tutorials are standalone + testsuite[name] = quote + using GeophysicalModelGenerator + $expr + end end - - @testset "GMT integration" begin - include("test_GMT.jl") - end - - @testset "Gmsh integration" begin - include("test_Gmsh.jl") - end - - @testset "Event counts" begin - include("test_event_counts.jl") - end - @testset "Create movie" begin - include("test_create_movie.jl") - end - - @testset "Sea level" begin - include("test_sea_level.jl") - end - - @testset "Waterflow" begin - include("test_WaterFlow.jl") - end - - @testset "ASAGI_IO" begin - include("test_ASAGI_IO.jl") - end - - @testset "LaMEM_post" begin - include("test_LaMEM_post_processing.jl") - end - end -# Cleanup -foreach(rm, filter(endswith(".vts"), readdir())) -foreach(rm, filter(endswith(".vtu"), readdir())) -rm("./markers/",recursive=true) +try + ParallelTestRunner.runtests(GeophysicalModelGenerator, ARGS; testsuite) +finally + # Cleanup + foreach(f -> rm(joinpath(@__DIR__, f)), filter(endswith(".vts"), readdir(@__DIR__))) + foreach(f -> rm(joinpath(@__DIR__, f)), filter(endswith(".vtu"), readdir(@__DIR__))) + if isdir(joinpath(@__DIR__, "markers")) + rm(joinpath(@__DIR__, "markers"), recursive = true) + end +end \ No newline at end of file diff --git a/test/test_files/ProcessorPartitioning_4cpu_1.2.2.bin b/test/test_files/ProcessorPartitioning_4cpu_1.2.2.bin deleted file mode 100644 index b411b71792600d86dff05d45c2a9ad4be67c77f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 856 zcmYL{OHKnZ5JVdgQI;GboTPoUITAOpmszr4!GgsIA%vnRf`o6v&*OgOShAnXo@sZN zZDuECc1pjjRIgK8OhauKwJ)YgZEfz)A<7)ihBhhZ=ri;bxwc|MWBzThe1Menota zxBM5n`|*R8FS>vE^Tob=`asJsy$|`eV_*Kgpyi|9kNjM*FJB+g@>lO`Wc?1x?;Esy LU!n`V3TpfZFhqw3 diff --git a/test/test_files/Subduction_VEP.dat b/test/test_files/Subduction_VEP.dat deleted file mode 100644 index 57bd5ac28..000000000 --- a/test/test_files/Subduction_VEP.dat +++ /dev/null @@ -1,273 +0,0 @@ -#=============================================================================== -# Scaling -#=============================================================================== - - units = geo - - unit_temperature = 1000 - unit_length = 1e6 - unit_viscosity = 1e20 - unit_stress = 1e10 - -#=============================================================================== -# Time stepping parameters -#=============================================================================== - - dt = 0.001 # time step - dt_max = 0.03 # maximum time step - dt_min = 0.0 - - nstep_max = 4 # maximum allowed number of steps (lower bound: time_end/dt_max) - nstep_out = 1 # save output every n steps - -#=============================================================================== -# Grid & discretization parameters -#=============================================================================== - -# Number of segments - - nseg_x = 1 - nseg_y = 1 - nseg_z = 3 - -# Number of cells for all segments - - nel_x = 64 - nel_y = 3 - nel_z = 33 90 5 - -# Coordinates of all segments (including start and end points) - - coord_x = -1000 1000 - coord_y = -30 30 - coord_z = -660 -400 0 20 - -#=============================================================================== -# Free surface -#=============================================================================== - - surf_use = 1 # free surface activation flag - surf_level = 0 # initial level - surf_air_phase = 0 # phase ID of sticky air layer - -#=============================================================================== -# Boundary conditions -#=============================================================================== - - - npath = 2 # Number of path points of Bezier curve (path-points only!) - theta = 0 0 # Orientation angles at path points (counter-clockwise positive) - time = 0 0.3 # Times at path points - path = 0 0 -15 0 # Path points x-y coordinates - npoly = 4 # Number of polygon vertices - poly = 790 -31 910 -31 910 31 790 31 # Polygon x-y coordinates at initial time - bot = -70 # Polygon bottom coordinate - top = 20 # Polygon top coordinate - - - open_top_bound = 1 - -# Temperature on the top and bottom boundaries - - temp_top = 0 - temp_bot = 1478; - -#=============================================================================== -# Solution parameters & controls -#=============================================================================== - - gravity = 0.0 0.0 -10.0 # gravity vector - FSSA = 1.0 # free surface stabilization parameter [0 - 1] - shear_heat_eff = 0.0 # shear heating efficiency parameter [0 - 1] - act_temp_diff = 1 # temperature diffusion activation flag - init_guess = 1 # initial guess flag -# p_lim_plast = 1 # limit pressure at first iteration for plasticity - eta_min = 1e18 # viscosity upper bound - eta_max = 1e24 # viscosity lower limit - eta_ref = 1e22 # reference viscosity (initial guess) - -#=============================================================================== -# Model setup & advection -#=============================================================================== - - msetup = files # setup type - nmark_x = 3 # markers per cell in x-direction - nmark_y = 3 # ... y-direction - nmark_z = 3 # ... z-direction - mark_load_file = ./markers/mdb # marker input file (extension is .xxxxxxxx.dat) - advect = basic # advection scheme - interp = stag # velocity interpolation scheme - mark_ctrl = subgrid -# nmark_lim = 16 100 # min/max number per cell - -#=============================================================================== -# Output -#=============================================================================== - -# Grid output options (output is always active) - - out_file_name = subduction # output file name - out_pvd = 1 # activate writing .pvd file - out_phase = 1 - out_density = 1 - out_visc_total = 1 - out_visc_creep = 1 - out_velocity = 1 - out_pressure = 1 - out_temperature = 1 - out_dev_stress = 1 - out_j2_dev_stress = 1 - out_strain_rate = 1 - out_j2_strain_rate = 1 - out_plast_strain = 1 - out_tot_displ = 1 - out_moment_res = 1 - out_cont_res = 1 - out_energ_res = 1 - -# Free surface output options - - out_surf = 1 # activate surface output - out_surf_pvd = 1 # activate writing .pvd file - out_surf_velocity = 1 - out_surf_topography = 1 - out_surf_amplitude = 1 - -#=============================================================================== -# Material phase parameters -#=============================================================================== - - # ------------------- water ------------------- - - ID = 0 - rho = 100 - eta = 1e18 - Cp = 1e6 - k = 100 - - - # ------------------- Mantle ------------------- - - ID = 1 - rho = 3300 - disl_prof = Dry_Olivine_disl_creep-Hirth_Kohlstedt_2003 - diff_prof = Dry_Olivine_diff_creep-Hirth_Kohlstedt_2003 - G = 7.4e10 - ch = 20e6 # cohesion [Pa] - fr = 30 # friction angle [deg] - Cp = 1.2e3 # heat capacity - k = 2.5 - alpha = 1e-5 - - - # ------------------- Weak channel ------------------- - - ID = 2 - rho = 3200 - disl_prof = Wet_Olivine-Ranalli_1995 - G = 7.4e10 - ch = 10e6 # cohesion [Pa] - fr = 5 # friction angle [deg] - Cp = 1.2e3 # heat capacity - k = 2.5 - alpha = 1e-5 - - - # ------------------- Oceanic crust ------------------- - - ID = 3 - rho = 2900 - disl_prof = Wet_Quarzite-Ranalli_1995 - G = 3.6e10 - ch = 20e6 # cohesion [Pa] - fr = 5 # friction angle [deg] - Cp = 1.2e3 # heat capacity - k = 2.5 - alpha = 1e-5 - - - # ------------------- Subducting plate ------------------- - - ID = 4 - rho = 3300 - disl_prof = Dry_Olivine_disl_creep-Hirth_Kohlstedt_2003 - G = 7.4e10 - ch = 20e6 # cohesion [Pa] - fr = 30 # friction angle [deg] - Cp = 1.2e3 # heat capacity - k = 2.5 - alpha = 1e-5 - - - # ------------------- Overriding plate ------------------- - - ID = 5 - rho = 3300 - disl_prof = Dry_Olivine_disl_creep-Hirth_Kohlstedt_2003 - G = 7.4e10 - ch = 20e6 # cohesion [Pa] - fr = 30 # friction angle [deg] - Cp = 1.2e3 # heat capacity - k = 2.5 - alpha = 1e-5 - - -#=============================================================================== -# PETSc options -#=============================================================================== - - - -# SNES - -snes_monitor - -snes_atol 1e-4 - -snes_rtol 1e-8 - -snes_stol 1e-16 - -snes_max_it 50 - -snes_max_funcs 500000 - -snes_max_linear_solve_fail 10000 - - -snes_Picard_max_it 10 - -snes_PicardSwitchToNewton_rtol 1e-2 - -snes_NewtonSwitchToPicard_it 5 - -snes_NewtonSwitchToPicard_rtol 1.1 - - -snes_linesearch_monitor - -snes_linesearch_type l2 - -snes_linesearch_maxstep 1.1 - -# Jacobian solver - - -js_ksp_type fgmres - -js_ksp_max_it 40 - -js_ksp_converged_reason - -js_ksp_monitor - -js_ksp_rtol 1e-6 - -js_ksp_atol 1e-8 - -# Preconditioner - - -pcmat_type mono - -jp_type mg - -gmg_pc_type mg - -gmg_pc_mg_levels 3 - -gmg_pc_mg_galerkin - -gmg_pc_mg_type multiplicative - -gmg_pc_mg_cycle_type v - -gmg_mg_levels_ksp_type richardson - -gmg_mg_levels_ksp_richardson_scale 0.5 - -gmg_mg_levels_ksp_max_it 20 - -gmg_mg_levels_pc_type jacobi - -crs_ksp_type preonly - -crs_pc_type lu - -crs_pc_factor_mat_solver_package mumps - -# 2D Multigrid !!! - - -da_refine_y 1 - - -objects_dump - - - -#=============================================================================== diff --git a/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr b/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr deleted file mode 100644 index 9816ed3c5..000000000 --- a/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output.pvtr +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr b/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr deleted file mode 100644 index e31cf0615..000000000 --- a/test/test_files/timestep/Timestep_00000000_0.00000000e+00/output_phase.pvtr +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr b/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr deleted file mode 100644 index 9816ed3c5..000000000 --- a/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output.pvtr +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr b/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr deleted file mode 100644 index e31cf0615..000000000 --- a/test/test_files/timestep/Timestep_00000010_1.49079279e+01/output_phase.pvtr +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/test/test_files/timestep/trackCartesianIndex(10, 10, 10).txt b/test/test_files/timestep/trackCartesianIndex(10, 10, 10).txt deleted file mode 100644 index e5b46c44e978a74bdc20e2601bcda7cd268dd0b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 zcmXr_@{wR+U|=v6U}SO0OfHf4o9Ds=km+Cm0Qko%D*ylh diff --git a/test/test_lamem.jl b/test/test_lamem.jl new file mode 100644 index 000000000..2ce1eab5d --- /dev/null +++ b/test/test_lamem.jl @@ -0,0 +1,193 @@ +# test LaMEM I/O routines +using Test, GeophysicalModelGenerator + +# Load LaMEM input file with grid refinement: +Grid = read_LaMEM_inputfile("test_files/non-uniform_grid.dat") +@test Grid.X[2358] ≈ 1.833333333333335 +@test Grid.Y[7741] ≈ -0.45 +@test Grid.Z[5195] ≈ -8.897114711471147 + +# Non-uniform grid in z-direction (taken from LaMEM test suite) +Grid = read_LaMEM_inputfile("test_files/Subduction_VEP.dat") +@test maximum(diff(Grid.z_vec)) ≈ 2.626262626262701 +@test minimum(diff(Grid.z_vec)) ≈ 1.3333333333333321 +@test maximum(diff(Grid.x_vec)) ≈ 10.4166666666667 +@test minimum(diff(Grid.x_vec)) ≈ 10.4166666666667 + +# Add command-line arguments +args = "-coord_z -660,-300,5,25 -nel_z 36,90,5 -nel_x 32" +coord_z = GeophysicalModelGenerator.ParseValue_LaMEM_InputFile("test_files/Subduction_VEP.dat", "coord_z", Float64, args = args) +nel_z = GeophysicalModelGenerator.ParseValue_LaMEM_InputFile("test_files/Subduction_VEP.dat", "nel_z", Int64, args = args) +@test coord_z ≈ [-660.0, -300.0, 5.0, 25.0] +@test nel_z == [36, 90, 5] + +Grid = read_LaMEM_inputfile("test_files/Subduction_VEP.dat", args = args) +@test Grid.nel_z == 131 + +# Load LaMEM input file: +Grid = read_LaMEM_inputfile("test_files/SaltModels.dat") +@test Grid.X[10] ≈ -2.40625 + +# Transfer into ParaviewData struct: +Phases = zeros(Int32, size(Grid.X)); +Temp = zeros(Float64, size(Grid.X)); +Model3D = CartData(Grid, (Phases = Grid.Z,)); +@test Value(Model3D.y[100]) == -1.9375km + +# Read Partitioning file: +PartitioningFile = "test_files/ProcessorPartitioning_4cpu_1.2.2.bin" +Nprocx, Nprocy, Nprocz, xc, yc, zc, nNodeX, nNodeY, nNodeZ = get_processor_partitioning(PartitioningFile) +@test Nprocz == 2 +@test yc[2] == 0.0 + +# Save serial output +save_LaMEM_markers_parallel(Model3D, verbose = false) + +# Save parallel output +save_LaMEM_markers_parallel(Model3D, PartitioningFile = PartitioningFile, verbose = false) + +# Get partitioning without generating partitioning file +n_ranks = 8; nx=128; ny=64; nz=32; +xcoords_ans=[-35.35034129014803, -19.867446943762857, -4.3845525973776835, 11.098341749007488, 26.58123609539267]; +ycoords_ans=[-24.510578171895816, 5.122856149231547, 34.75629047035891]; +zcoords_ans=[-6.4, 6.4]; +nProcX_ans = 4; nProcY_ans = 2; nProcZ_ans = 1; +nNodeX_ans = 129; nNodeY_ans = 65; nNodeZ_ans = 33; +P = setup_model_domain([extrema(xcoords_ans)...], [extrema(ycoords_ans)...], [extrema(zcoords_ans)...], nx, ny, nz, n_ranks) +@test isapprox(nProcX_ans, P.nProcX; atol=1.0e-8) +@test isapprox(nProcY_ans, P.nProcY; atol=1.0e-8) +@test isapprox(nProcZ_ans, P.nProcZ; atol=1.0e-8) +@test isapprox(xcoords_ans, P.xc; atol=1.0e-8) +@test isapprox(ycoords_ans, P.yc; atol=1.0e-8) +@test isapprox(zcoords_ans, P.zc; atol=1.0e-8) +@test isapprox(nNodeX_ans, P.nNodeX; atol=1.0e-8) +@test isapprox(nNodeY_ans, P.nNodeY; atol=1.0e-8) +@test isapprox(nNodeZ_ans, P.nNodeZ; atol=1.0e-8) + +n_ranks = 128; nx=256; ny=1; nz=128; +xcoords_ans=[-35.35034129014803, -31.479617703551735, -27.608894116955444, -23.73817053035915, -19.867446943762857, -15.996723357166562, -12.125999770570267, -8.255276183973974, -4.384552597377681, -0.5138290107813872, 3.3568945758149065, 7.227618162411201, 11.098341749007494, 14.969065335603787, 18.839788922200082, 22.710512508796374, 26.58123609539267]; +ycoords_ans=[-0.1, 0.1]; +zcoords_ans=[-6.4, -4.8, -3.2, -1.6, 0.0, 1.6, 3.2, 4.8, 6.4]; +nProcX_ans = 16; nProcY_ans = 1; nProcZ_ans = 8; +nNodeX_ans = 257; nNodeY_ans = 2; nNodeZ_ans = 129; +P = setup_model_domain([extrema(xcoords_ans)...], [extrema(ycoords_ans)...], [extrema(zcoords_ans)...], nx, ny, nz, n_ranks) +@test isapprox(nProcX_ans, P.nProcX; atol=1.0e-8) +@test isapprox(nProcY_ans, P.nProcY; atol=1.0e-8) +@test isapprox(nProcZ_ans, P.nProcZ; atol=1.0e-8) +@test isapprox(xcoords_ans, P.xc; atol=1.0e-8) +@test isapprox(ycoords_ans, P.yc; atol=1.0e-8) +@test isapprox(zcoords_ans, P.zc; atol=1.0e-8) +@test isapprox(nNodeX_ans, P.nNodeX; atol=1.0e-8) +@test isapprox(nNodeY_ans, P.nNodeY; atol=1.0e-8) +@test isapprox(nNodeZ_ans, P.nNodeZ; atol=1.0e-8) + +n_ranks = 2048; nx=512; ny=2048; nz=512; +xcoords_ans=[-35.35034129014803, -27.60889411695544, -19.867446943762857, -12.12599977057027, -4.3845525973776835, 3.356894575814903, 11.098341749007488, 18.839788922200068, 26.58123609539267 ]; +ycoords_ans=[-24.510578171895816, -22.658488526825355, -20.806398881754898, -18.954309236684434, -17.102219591613977, -15.250129946543517, -13.398040301473054, -11.545950656402596, -9.693861011332135, -7.841771366261674]; +zcoords_ans=[-6.4, -4.800000000000001, -3.2, -1.6, 0.0, 1.6, 3.2, 4.800000000000001, 6.4]; +nProcX_ans = 8; nProcY_ans = 32; nProcZ_ans = 8; +nNodeX_ans = 513; nNodeY_ans = 2049; nNodeZ_ans = 513; +P = setup_model_domain([-35.35034129014803,26.58123609539267], [-24.510578171895816,34.75629047035891], [-6.4,6.4], nx, ny, nz, n_ranks) +@test isapprox(nProcX_ans, P.nProcX; atol=1.0e-8) +@test isapprox(nProcY_ans, P.nProcY; atol=1.0e-8) +@test isapprox(nProcZ_ans, P.nProcZ; atol=1.0e-8) +@test isapprox(xcoords_ans, P.xc; atol=1.0e-8) +@test isapprox(ycoords_ans, P.yc[1:10]; atol=1.0e-8) +@test isapprox(zcoords_ans, P.zc; atol=1.0e-8) +@test isapprox(nNodeX_ans, P.nNodeX; atol=1.0e-8) +@test isapprox(nNodeY_ans, P.nNodeY; atol=1.0e-8) +@test isapprox(nNodeZ_ans, P.nNodeZ; atol=1.0e-8) + +n_ranks = 32768; nx=2048; ny=512; nz=1024; +xcoords_ans=[-35.35034129014803, -34.38266039349895, -33.41497949684988, -32.4472986002008, -31.47961770355173, -30.511936806902664, -29.54425591025359, -28.576575013604515, -27.60889411695544, -26.64121322030637 ]; +ycoords_ans=[ -24.510578171895816, -20.806398881754898, -17.102219591613977, -13.398040301473054, -9.693861011332135, -5.989681721191215, -2.285502431050293, 1.418676859090624, 5.122856149231547, 8.82703543937247]; +zcoords_ans=[-6.4, -6.0, -5.6000000000000005, -5.2, -4.800000000000001, -4.4, -4.0, -3.6, -3.2, -2.8000000000000003]; +nProcX_ans = 64; nProcY_ans = 16; nProcZ_ans = 32; +nNodeX_ans = 2049; nNodeY_ans = 513; nNodeZ_ans = 1025; +P = setup_model_domain([-35.35034129014803,26.58123609539267], [-24.510578171895816,34.75629047035891], [-6.4,6.4], nx, ny, nz, n_ranks) +@test isapprox(nProcX_ans, P.nProcX; atol=1.0e-8) +@test isapprox(nProcY_ans, P.nProcY; atol=1.0e-8) +@test isapprox(nProcZ_ans, P.nProcZ; atol=1.0e-8) +@test isapprox(xcoords_ans, P.xc[1:10]; atol=1.0e-8) +@test isapprox(ycoords_ans, P.yc[1:10]; atol=1.0e-8) +@test isapprox(zcoords_ans, P.zc[1:10]; atol=1.0e-8) +@test isapprox(nNodeX_ans, P.nNodeX; atol=1.0e-8) +@test isapprox(nNodeY_ans, P.nNodeY; atol=1.0e-8) +@test isapprox(nNodeZ_ans, P.nNodeZ; atol=1.0e-8) + +# Test creating model setups +Grid = read_LaMEM_inputfile("test_files/Subduction2D_FreeSlip_Particles_Linear_DirectSolver.dat") +Phases = zeros(Int32, size(Grid.X)); + +# constant T +Temp = ones(Float64, size(Grid.X)) * 1350; +add_box!(Phases, Temp, Grid, xlim = (0, 500), zlim = (-50, 0), phase = ConstantPhase(3), DipAngle = 10, T = ConstantTemp(1000)) +@test sum(Temp) == 1.1905107e9 + +# Add a layer above the slab with a different phase but no thermal structure +add_box!(Phases, Temp, Grid, xlim = (0, 500), zlim = (0, 20), phase = ConstantPhase(1), DipAngle = 10, Origin = (0, 0, 0)) +@test sum(Temp) == 1.1905107e9 + +# Linear T +Temp = ones(Float64, size(Grid.X)) * 1350; +add_box!(Phases, Temp, Grid, xlim = (0, 500), zlim = (-50, 0), phase = ConstantPhase(3), DipAngle = 10, T = LinearTemp(Tbot = 1350, Ttop = 200)) +@test sum(Temp) == 1.1879553307069368e9 + +# Halfspace cooling T structure +Phases = zeros(Int32, size(Grid.X)); +Temp = ones(Float64, size(Grid.X)) * 1350; +add_box!(Phases, Temp, Grid, xlim = (0, 500), zlim = (-500, 0), phase = LithosphericPhases(Layers = [15 15 250], Phases = [1 2 3 0], Tlab = 1250), DipAngle = 10, T = HalfspaceCoolingTemp(Age = 20, Adiabat = 0.3)) +@test sum(Temp) == 1.194094895654067e9 + +# Mid-oceanic ridge cooling temperature structure +Phases = zeros(Int32, size(Grid.X)); +Temp = ones(Float64, size(Grid.X)) * 1350; +add_box!(Phases, Temp, Grid, xlim = (0, 500), zlim = (-500, -20), phase = LithosphericPhases(Layers = [15 15 250], Phases = [1 2 3 0], Tlab = 1250), DipAngle = 10, T = SpreadingRateTemp(MORside = "right", SpreadingVel = 3)) +@test sum(Temp) == 1.189394358568891e9 + +Model3D = CartData(Grid, (Phases = Phases, Temp = Temp)); +write_paraview(Model3D, "LaMEM_ModelSetup") # Save model to paraview + + +# Test writing a LaMEM topography file +X, Y, Z = xyz_grid(-20:20, -10:10, 0); +Z = cos.(2 * pi .* X ./ 5) .* cos.(2 * pi .* Y ./ 10) + +Topo = CartData(X, Y, Z, (Topography = Z,)) +@test save_LaMEM_topography(Topo, "test_topo.dat") == nothing +rm("test_topo.dat") + + +# Test adding geometric primitives +Grid = read_LaMEM_inputfile("test_files/GeometricPrimitives.dat") +Phases = zeros(Int32, size(Grid.X)); +Temp = zeros(Float64, size(Grid.X)); +add_sphere!(Phases, Temp, Grid, cen = (0, 0, -6), radius = 2.0, phase = ConstantPhase(1), T = ConstantTemp(800)) +@test Phases[55, 55, 55] == 1 +@test Phases[56, 56, 56] == 0 +@test Temp[44, 52, 21] == 800.0 +@test Temp[44, 52, 20] == 0.0 + +add_ellipsoid!(Phases, Temp, Grid, cen = (-2, -1, -7), axes = (1, 2, 3), StrikeAngle = 90, DipAngle = 45, phase = ConstantPhase(2), T = ConstantTemp(600)) +@test Phases[11, 37, 28] == 2 +@test Phases[10, 37, 28] == 0 +@test Temp[31, 58, 18] == 600.0 +@test Temp[31, 59, 18] == 0.0 + +add_cylinder!(Phases, Temp, Grid, base = (0, 0, -5), cap = (3, 3, -2), radius = 1.5, phase = ConstantPhase(3), T = ConstantTemp(400)) +@test Phases[55, 65, 75] == 3 +@test Phases[54, 65, 75] == 0 +@test Temp[55, 46, 45] == 400.0 +@test Temp[55, 45, 45] == 800.0 + +# for debugging: +#data = CartData(Grid, (;Phases, Temp)); +#write_paraview(data,"test") + +# test adding generic volcano topography +Grid = read_LaMEM_inputfile("test_files/SaltModels.dat"); +Topo = make_volc_topo(Grid, center = [0.0, 0.0], height = 0.4, radius = 1.5, crater = 0.5, base = 0.1); +@test Topo.fields.Topography[13, 13] ≈ 0.279583654km +Topo = make_volc_topo(Grid, center = [0.0, 0.0], height = 0.8, radius = 0.5, crater = 0.0, base = 0.4, background = Topo.fields.Topography); +@test Topo.fields.Topography[13, 13] ≈ 0.279583654km +@test Topo.fields.Topography[16, 18] ≈ 0.619722436km \ No newline at end of file From a0244437c266bc927308d21e4fd5afc4f690a1a5 Mon Sep 17 00:00:00 2001 From: TatjanaWeiler <152634226+TatjanaWeiler@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:47:40 +0200 Subject: [PATCH 16/16] add poly temperature correction --- src/Setup_geometry.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Setup_geometry.jl b/src/Setup_geometry.jl index 084a525d7..ec4460eca 100644 --- a/src/Setup_geometry.jl +++ b/src/Setup_geometry.jl @@ -799,14 +799,17 @@ function add_polygon!( end end + if !isempty(ind) + Zrel = Z .- maximum(zlim); -# Compute thermal structure accordingly. See routines below for different options -if T != nothing - Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Z[ind], Phase[ind], T) -end + # Compute thermal structure accordingly. See routines below for different options + if T != nothing + Temp[ind] = compute_thermal_structure(Temp[ind], X[ind], Y[ind], Zrel[ind], Phase[ind], T) + end -# Set the phase. Different routines are available for that - see below. -Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + # Set the phase. Different routines are available for that - see below. + Phase[ind] = compute_phase(Phase[ind], Temp[ind], X[ind], Y[ind], Z[ind], phase) + end return nothing end