From d428e5be9d3b52d6e4791854d2cf268dbadeb0bd Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Fri, 4 Sep 2020 19:31:47 +0300 Subject: [PATCH 01/25] Removed _popular_species and _species_symbol_map dicts, added setters to SOLPSSimulation, other fixes. --- cherab/solps/formats/balance.py | 87 +++---- cherab/solps/formats/mdsplus.py | 202 +++++++-------- cherab/solps/formats/raw_simulation_files.py | 78 +++--- cherab/solps/solps_plasma.py | 248 +++++++++++-------- 4 files changed, 306 insertions(+), 309 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 90e4ed9..045496b 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -19,36 +19,24 @@ import numpy as np from scipy.io import netcdf -from scipy.constants import elementary_charge +from scipy import constants from raysect.core.math.function.float import Discrete2DMesh from cherab.core.math.mappers import AxisymmetricMapper -from cherab.core.atomic.elements import hydrogen, deuterium, helium, beryllium, carbon, nitrogen, oxygen, neon, \ - argon, krypton, xenon +from cherab.core.atomic.elements import lookup_isotope, deuterium from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation - -Q = elementary_charge - -# key is nuclear charge Z and atomic mass AMU -_popular_species = { - (1, 2): deuterium, - (6, 12.0): carbon, - (2, 4.003): helium, - (7, 14.0): nitrogen, - (10, 20.180): neon, - (18, 39.948): argon, - (18, 40.0): argon, - (36, 83.798): krypton, - (54, 131.293): xenon -} +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element + def load_solps_from_balance(balance_filename): """ Load a SOLPS simulation from SOLPS balance.nc output files. """ + el_charge = constants.elementary_charge + rydberg_energy = constants.value('Rydberg constant times hc in eV') + # Open the file fhandle = netcdf.netcdf_file(balance_filename, 'r') @@ -62,26 +50,15 @@ def load_solps_from_balance(balance_filename): cr_z = np.moveaxis(cr_z, 0, -1) # Create the SOLPS mesh - mesh = SOLPSMesh(cr_x, cr_z, vol) - - sim = SOLPSSimulation(mesh) - - # TODO: add code to load SOLPS velocities and magnetic field from files - - # Load electron species - sim._electron_temperature = fhandle.variables['te'].data.copy() / Q - sim._electron_density = fhandle.variables['ne'].data.copy() + mesh = SOLPSMesh(cr_x, cr_z, vol) - ########################################## - # Load each plasma species in simulation # - ########################################## + # Load each plasma species in simulation - sim._species_list = [] + species_list = [] n_species = len(fhandle.variables['am'].data) - for i in range(n_species): - # Extract the nuclear charge + # Extract the nuclear charge if fhandle.variables['species'].data[i, 1] == b'D': zn = 1 if fhandle.variables['species'].data[i, 1] == b'C': @@ -93,25 +70,29 @@ def load_solps_from_balance(balance_filename): if fhandle.variables['species'].data[i, 1] == b'A' and fhandle.variables['species'].data[i, 2] == b'r': zn = 18 - am = float(fhandle.variables['am'].data[i]) # Atomic mass + am = int(round(float(fhandle.variables['am'].data[i]))) # Atomic mass charge = int(fhandle.variables['za'].data[i]) # Ionisation/charge - species = _popular_species[(zn, am)] + isotope = lookup_isotope(zn, number=am) + species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same - # If we only need to populate species_list, there is probably a faster way to do this... - sim.species_list.append(species.symbol + str(charge)) + # If we only need to populate species_list, there is probably a faster way to do this... + species_list.append((species, charge)) + + sim = SOLPSSimulation(mesh, species_list) + + # TODO: add code to load SOLPS velocities and magnetic field from files + + # Load electron species + sim.electron_temperature = fhandle.variables['te'].data.copy() / el_charge + sim.electron_density = fhandle.variables['ne'].data.copy() tmp = fhandle.variables['na'].data.copy() tmp = np.moveaxis(tmp, 0, -1) - sim._species_density = tmp - - # Make Mesh Interpolator function for inside/outside mesh test. - inside_outside_data = np.ones(mesh.num_tris) - inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coords, mesh.triangles, inside_outside_data, limit=False)) - sim._inside_mesh = inside_outside + sim.species_density = tmp # Load the neutrals data try: - D0_indx = sim.species_list.index('D0') + D0_indx = sim.species_list.index((deuterium, 0)) except ValueError: D0_indx = None @@ -121,7 +102,7 @@ def load_solps_from_balance(balance_filename): if D0_indx is not None: b2_len = np.shape(sim.species_density[:, :, D0_indx])[-1] eirene_len = np.shape(fhandle.variables['dab2'].data)[-1] - sim.species_density[:, :, D0_indx] = fhandle.variables['dab2'].data[0, :, 0:b2_len-eirene_len] + sim.species_density[:, :, D0_indx] = fhandle.variables['dab2'].data[0, :, 0:b2_len - eirene_len] eirene_run = True else: @@ -138,20 +119,20 @@ def load_solps_from_balance(balance_filename): # Ionisation rate from EIRENE, needed to calculate the energy loss to overcome the ionisation potential of atoms if 'eirene_mc_papl_sna_bal' in fhandle.variables.keys(): - eirene_potential_loss = 13.6 * np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1, :, :] * Q / vol + eirene_potential_loss = rydberg_energy * np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1, :, :] * el_charge / vol # This will be negative (energy sink); multiply by -1 - sim._total_rad = -1.0 * (b2_ploss + (eirene_ecoolrate-eirene_potential_loss)) + sim.total_radiation = -1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss)) else: - # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0)/vol + # Total radiated power from B2, not including neutrals + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / vol - potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0)/vol + potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0) / vol # Save total radiated power to the simulation object - sim._total_rad = 13.6 * Q * potential_loss - b2_ploss + sim.total_radiation = rydberg_energy * el_charge * potential_loss - b2_ploss - fhandle.close() + fhandle.close() return sim diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index bc9e3ae..e728ae1 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -19,17 +19,16 @@ import re import numpy as np -from math import sqrt from raysect.core import Point2D -from raysect.core.math.function.float import Discrete2DMesh -from cherab.core.math.mappers import AxisymmetricMapper +from cherab.core.atomic.elements import lookup_element, lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh from cherab.solps.solps_plasma import SOLPSSimulation _SPECIES_REGEX = '([a-zA-z]+)\+?([0-9]+)' + # TODO: violates interface of SOLPSSimulation.... puts numpy arrays in the object where they should be function2D def load_solps_from_mdsplus(mds_server, ref_number): """ @@ -48,111 +47,99 @@ def load_solps_from_mdsplus(mds_server, ref_number): # Load SOLPS mesh geometry and lookup arrays mesh = load_mesh_from_mdsplus(conn) - sim = SOLPSSimulation(mesh) + + # Load each plasma species + # Master list of species, e.g. ['D0', 'D+1', 'C0', 'C+1', ... + master_list = conn.get('\SOLPS::TOP.IDENT.SPECIES').data() + try: + master_list = master_list.decode('UTF-8').split() + except AttributeError: # Already a string + master_list = master_list.split() + + species_list = [] + for sp in master_list: + # Only hydrogen isotopes (D, T) are supported in TOP.IDENT.SPECIES for now (no He3, C13, etc.), + # so re.match should be safe. + symbol, charge = re.match(_SPECIES_REGEX, sp).groups() + try: + species = lookup_element(symbol) + except ValueError: + species = lookup_isotope(symbol) + species_list.append((species, int(charge))) + + sim = SOLPSSimulation(mesh, species_list) ni = mesh.nx nj = mesh.ny ########################## # Magnetic field vectors # - raw_b_field = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2) + b_field_vectors = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2)[:, :, :3] b_field_vectors_cartesian = np.zeros((ni, nj, 3)) - b_field_vectors = np.zeros((ni, nj, 3)) - for i in range(ni): - for j in range(nj): - bparallel = raw_b_field[i, j, 0] - bradial = raw_b_field[i, j, 1] - btoroidal = raw_b_field[i, j, 2] - b_field_vectors[i, j] = (bparallel, bradial, btoroidal) - - pv = mesh.poloidal_grid_basis[i, j, 0] # parallel basis vector - rv = mesh.poloidal_grid_basis[i, j, 1] # radial basis vector - - bx = pv.x * bparallel + rv.x * bradial # component of B along poloidal x - by = pv.y * bparallel + rv.y * bradial # component of B along poloidal y - b_field_vectors_cartesian[i, j] = (bx, btoroidal, by) - sim._b_field_vectors = b_field_vectors - sim._b_field_vectors_cartesian = b_field_vectors_cartesian - - # Load electron species - sim._electron_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1) # (32, 98) => (98, 32) - sim._electron_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NE').data(), 0, 1) # (32, 98) => (98, 32) - - ############################ - # Load each plasma species # - ############################ - # Master list of species, e.g. ['D0', 'D+1', 'C0', 'C+1', ... - species_list = conn.get('\SOLPS::TOP.IDENT.SPECIES').data() - try: - species_list = species_list.decode('UTF-8') - except AttributeError: # Already a string - pass - sim._species_list = species_list.split() - sim._species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) - sim._rad_par_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) # radial particle flux - sim._radial_area = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.SY').data(), 0, 1) # radial contact area + bparallel = b_field_vectors[:, :, 0] + bradial = b_field_vectors[:, :, 1] + btoroidal = b_field_vectors[:, :, 2] + + vec_x = np.vectorize(lambda obj: obj.x) + vec_y = np.vectorize(lambda obj: obj.y) - # Load the neutral atom density from B2 + pvx = vec_x(mesh.poloidal_grid_basis[:, :, 0]) # x-coordinate of parallel basis vector + pvy = vec_y(mesh.poloidal_grid_basis[:, :, 0]) # y-coordinate of parallel basis vector + rvx = vec_x(mesh.poloidal_grid_basis[:, :, 1]) # x-coordinate of radial basis vector + rvy = vec_y(mesh.poloidal_grid_basis[:, :, 1]) # y-coordinate of radial basis vector + + b_field_vectors_cartesian[:, :, 0] = pvx * bparallel + rvx * bradial # component of B along poloidal x + b_field_vectors_cartesian[:, :, 2] = pvy * bparallel + rvy * bradial # component of B along poloidal y + b_field_vectors_cartesian[:, :, 1] = btoroidal + + sim.b_field = b_field_vectors + sim.b_field_cartesian = b_field_vectors_cartesian + + # Load electron temperature and density + sim.electron_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1) # (32, 98) => (98, 32) + sim.electron_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NE').data(), 0, 1) # (32, 98) => (98, 32) + + # Load species + sim.species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) + sim.radial_particle_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) # radial particle flux + sim.radial_area = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.SY').data(), 0, 1) # radial contact area + + # Load the neutral atom density from Eirene if available dab2 = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() if isinstance(dab2, np.ndarray): - sim._b2_neutral_densities = np.swapaxes(dab2, 0, 2) + # Replace the species densities + neutral_densities = np.swapaxes(dab2, 0, 2) + + neutral_i = 0 # counter for neutral atoms + for k, sp in enumerate(sim.species_list): + charge = sp[1] + if charge == 0: + sim.species_density[:, :, k] = neutral_densities[:, :, neutral_i] + neutral_i += 1 - sim._velocities_parallel = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) - sim._velocities_radial = np.zeros((ni, nj, len(sim.species_list))) - sim._velocities_toroidal = np.zeros((ni, nj, len(sim.species_list))) - sim._velocities_cartesian = np.zeros((ni, nj, len(sim.species_list), 3), dtype=np.float64) + sim.velocities_parallel = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) + sim.velocities_radial = np.zeros((ni, nj, len(sim.species_list))) + sim.velocities_toroidal = np.zeros((ni, nj, len(sim.species_list))) + sim.velocities_cartesian = np.zeros((ni, nj, len(sim.species_list), 3)) ################################################ # Calculate the species' velocity distribution # - b2_neutral_i = 0 # counter for B2 neutrals - for k, sp in enumerate(sim.species_list): - # Identify the species based on its symbol - symbol, charge = re.match(_SPECIES_REGEX, sp).groups() - charge = int(charge) + # calculate field component ratios for velocity conversion + bplane = np.sqrt(bparallel**2 + btoroidal**2) + parallel_to_toroidal_ratio = bparallel * btoroidal / (bplane**2) - # If neutral and B" atomic density available, use B2 density, otherwise use fluid species density. - if charge == 0 and (sim._b2_neutral_densities is not None): - species_dens_data = sim.b2_neutral_densities[:, :, b2_neutral_i] - b2_neutral_i += 1 - else: - species_dens_data = sim.species_density[:, :, k] - - for i in range(ni): - for j in range(nj): - # Load grid basis vectors - pv = mesh.poloidal_grid_basis[i, j, 0] # parallel basis vector - rv = mesh.poloidal_grid_basis[i, j, 1] # radial basis vector - - # calculate field component ratios for velocity conversion - bparallel = b_field_vectors[i, j, 0] - btoroidal = b_field_vectors[i, j, 2] - bplane = sqrt(bparallel**2 + btoroidal**2) - parallel_to_toroidal_ratio = bparallel * btoroidal / (bplane**2) - - # Calculate toroidal and radial velocity components - v_parallel = sim.velocities_parallel[i, j, k] # straight from SOLPS 'UA' variable - v_toroidal = v_parallel * parallel_to_toroidal_ratio - sim.velocities_toroidal[i, j, k] = v_toroidal - # Special case for edge of mesh, no radial velocity expected. - try: - if species_dens_data[i, j] == 0: - v_radial = 0.0 - else: - v_radial = sim.radial_particle_flux[i, j, k] / sim.radial_area[i, j] / species_dens_data[i, j] - except IndexError: - v_radial = 0.0 - sim.velocities_radial[i, j, k] = v_radial - - # Convert velocities to cartesian coordinates - vx = pv.x * v_parallel + rv.x * v_radial # component of v along poloidal x - vy = pv.y * v_parallel + rv.y * v_radial # component of v along poloidal y - sim.velocities_cartesian[i, j, k, :] = (vx, v_toroidal, vy) - - # Make Mesh Interpolator function for inside/outside mesh test. - inside_outside_data = np.ones(mesh.num_tris) - inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coords, mesh.triangles, inside_outside_data, limit=False)) - sim._inside_mesh = inside_outside + # Calculate toroidal and radial velocity components + sim.velocities_toroidal = sim.velocities_parallel * parallel_to_toroidal_ratio[:, :, None] + + for k, sp in enumerate(sim.species_list): + i, j = np.where(sim.species_density[:, :-1, k] > 0) + sim.velocities_radial[i, j, k] = sim.radial_particle_flux[i, j, k] / sim.radial_area[i, j] / sim.species_density[i, j, k] + + # Convert velocities to cartesian coordinates + sim.velocities_cartesian[:, :, :, 0] = pvx[:, :, None] * sim.velocities_parallel + rvx[:, :, None] * sim.velocities_radial # component of v along poloidal x + sim.velocities_cartesian[:, :, :, 2] = pvy[:, :, None] * sim.velocities_parallel + rvy[:, :, None] * sim.velocities_radial # component of v along poloidal y + sim.velocities_cartesian[:, :, :, 1] = sim.velocities_toroidal ############################### # Load extra data from server # @@ -174,12 +161,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): else: neurad = np.zeros(brmrad.shape) - total_rad_data = np.zeros(vol.shape) - ni, nj = vol.shape - for i in range(ni): - for j in range(nj): - total_rad_data[i, j] = (linerad[i, j] + brmrad[i, j] + neurad[i, j]) / vol[i, j] - sim._total_rad = total_rad_data + sim.total_radiation = (linerad + brmrad + neurad) / vol return sim @@ -226,28 +208,28 @@ def load_mesh_from_mdsplus(mds_connection): if i == nx - 1: # Special case for end of array, repeat previous calculation. # This is because I don't have access to the gaurd cells. - xp_x = cr[i, j] - cr[i-1, j] - xp_y = cz[i, j] - cz[i-1, j] + xp_x = cr[i, j] - cr[i - 1, j] + xp_y = cz[i, j] - cz[i - 1, j] norm = np.sqrt(xp_x**2 + xp_y**2) - cell_poloidal_basis[i, j, 0] = Point2D(xp_x/norm, xp_y/norm) + cell_poloidal_basis[i, j, 0] = Point2D(xp_x / norm, xp_y / norm) else: - xp_x = cr[i+1, j] - cr[i, j] - xp_y = cz[i+1, j] - cz[i, j] + xp_x = cr[i + 1, j] - cr[i, j] + xp_y = cz[i + 1, j] - cz[i, j] norm = np.sqrt(xp_x**2 + xp_y**2) - cell_poloidal_basis[i, j, 0] = Point2D(xp_x/norm, xp_y/norm) + cell_poloidal_basis[i, j, 0] = Point2D(xp_x / norm, xp_y / norm) # Work out cell's 2D radial vector in the poloidal plane if j == ny - 1: # Special case for end of array, repeat previous calculation. - yr_x = cr[i, j] - cr[i, j-1] - yr_y = cz[i, j] - cz[i, j-1] + yr_x = cr[i, j] - cr[i, j - 1] + yr_y = cz[i, j] - cz[i, j - 1] norm = np.sqrt(yr_x**2 + yr_y**2) - cell_poloidal_basis[i, j, 1] = Point2D(yr_x/norm, yr_y/norm) + cell_poloidal_basis[i, j, 1] = Point2D(yr_x / norm, yr_y / norm) else: - yr_x = cr[i, j+1] - cr[i, j] - yr_y = cz[i, j+1] - cz[i, j] + yr_x = cr[i, j + 1] - cr[i, j] + yr_y = cz[i, j + 1] - cz[i, j] norm = np.sqrt(yr_x**2 + yr_y**2) - cell_poloidal_basis[i, j, 1] = Point2D(yr_x/norm, yr_y/norm) + cell_poloidal_basis[i, j, 1] = Point2D(yr_x / norm, yr_y / norm) mesh._poloidal_grid_basis = cell_poloidal_basis diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index ea0354d..31b9494 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -19,33 +19,16 @@ import os import numpy as np +from scipy.constants import elementary_charge from raysect.core.math.function.float import Discrete2DMesh from cherab.core.math.mappers import AxisymmetricMapper -from cherab.core.atomic.elements import hydrogen, deuterium, helium, beryllium, carbon, nitrogen, oxygen, neon, \ - argon, krypton, xenon +from cherab.core.atomic.elements import lookup_isotope from cherab.solps.eirene import load_fort44_file from cherab.solps.b2.parse_b2_block_file import load_b2f_file from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation - -Q = 1.602E-19 - -# key is nuclear charge Z and atomic mass AMU -_popular_species = { - (1, 2): deuterium, - (2, 4.003): helium, - (2, 4.0): helium, - (6, 12.011): carbon, - (6, 12.0): carbon, - (7, 14.007): nitrogen, - (10, 20.0): neon, - (10, 20.1797): neon, - (18, 39.948): argon, - (36, 83.798): krypton, - (54, 131.293): xenon -} +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element # Code based on script by Felix Reimold (2016) @@ -83,48 +66,49 @@ def load_solps_from_raw_output(simulation_path, debug=False): header_dict, sim_info_dict, mesh_data_dict = load_b2f_file(b2_state_file, debug=debug) - sim = SOLPSSimulation(mesh) - ni = mesh.nx - nj = mesh.ny - - # TODO: add code to load SOLPS velocities and magnetic field from files - - # Load electron species - sim._electron_temperature = mesh_data_dict['te']/Q - sim._electron_density = mesh_data_dict['ne'] - - ########################################## - # Load each plasma species in simulation # - ########################################## - - sim._species_list = [] + # Load each plasma species in simulation + species_list = [] for i in range(len(sim_info_dict['zn'])): zn = int(sim_info_dict['zn'][i]) # Nuclear charge - am = float(sim_info_dict['am'][i]) # Atomic mass + am = int(round(float(sim_info_dict['am'][i]))) # Atomic mass number charge = int(sim_info_dict['zamax'][i]) # Ionisation/charge - species = _popular_species[(zn, am)] - sim.species_list.append(species.symbol + str(charge)) + isotope = lookup_isotope(zn, number=am) + species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same + species_list.append((species, charge)) - sim._species_density = mesh_data_dict['na'] + sim = SOLPSSimulation(mesh, species_list) - # Make Mesh Interpolator function for inside/outside mesh test. - inside_outside_data = np.ones(mesh.num_tris) - inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coords, mesh.triangles, inside_outside_data, limit=False)) - sim._inside_mesh = inside_outside + # TODO: add code to load SOLPS velocities and magnetic field from files + + # Load electron species + sim.electron_temperature = mesh_data_dict['te'] / elementary_charge + sim.electron_density = mesh_data_dict['ne'] + + sim.species_density = mesh_data_dict['na'] # Load total radiated power from EIRENE output file eirene = load_fort44_file(eirene_fort44_file, debug=debug) - sim._eirene = eirene + sim.eirene_simulation = eirene # Note EIRENE data grid is slightly smaller than SOLPS grid, for example (98, 38) => (96, 36) # Need to pad EIRENE data to fit inside larger B2 array nx = mesh.nx ny = mesh.ny + + # Replacing B2 neutral densities with EIRENE ones + da_raw_data = eirene.da + neutral_i = 0 # counter for neutral atoms + for k, sp in enumerate(sim.species_list): + charge = sp[1] + if charge == 0: + sim.species_density[1:-1, 1:-1, k] = da_raw_data[:, :, neutral_i] + neutral_i += 1 + + # Obtaining total radiation eradt_raw_data = eirene.eradt.sum(2) - eradt_data = np.zeros((nx, ny)) - eradt_data[1:nx-1, 1:ny-1] = eradt_raw_data - sim._total_rad = eradt_data + sim.total_radiation = np.zeros((nx, ny)) + sim.total_radiation[1:-1, 1:-1] = eradt_raw_data return sim diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 4b21488..4cb69c8 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -17,7 +17,6 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -import re import pickle import numpy as np import matplotlib.pyplot as plt @@ -32,60 +31,65 @@ # CHERAB core imports from cherab.core import Plasma, Species, Maxwellian from cherab.core.math.mappers import AxisymmetricMapper -from cherab.core.atomic.elements import ( - hydrogen, deuterium, helium, beryllium, carbon, boron, nitrogen, oxygen, neon, - argon, krypton, xenon -) # This SOLPS package imports +from cherab.solps.eirene import Eirene from .solps_3d_functions import SOLPSFunction3D, SOLPSVectorFunction3D from .mesh_geometry import SOLPSMesh -Q = 1.602E-19 - -_species_symbol_map = { - 'D': deuterium, - 'C': carbon, - 'He': helium, - 'B': boron, - 'N': nitrogen, - 'Ne': neon, - 'Ar': argon, - 'Kr': krypton, - 'Xe': xenon, -} - -_SPECIES_REGEX = '([a-zA-z]+)\+?([0-9]+)' - # TODO: this interface is half broken - some routines expect internal data as arrays, others as function 3d class SOLPSSimulation: - def __init__(self, mesh): + def __init__(self, mesh, species_list): + + # Mesh and species_list cannot be changed after initialisation + if isinstance(mesh, SOLPSMesh): + self._mesh = mesh + else: + raise ValueError('Argument "mesh" must be a SOLPSMesh instance.') + + # Make Mesh Interpolator function for inside/outside mesh test. + inside_outside_data = np.ones(mesh.num_tris) + inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coords, mesh.triangles, inside_outside_data, limit=False)) + self._inside_mesh = inside_outside - self.mesh = mesh + if not len(species_list): + raise ValueError('Argument "species_list" must contain at least one species.') + self._species_list = tuple(species_list) # adding species is not allowed self._electron_temperature = None self._electron_density = None - self._species_list = None self._species_density = None - self._rad_par_flux = None + self._radial_particle_flux = None self._radial_area = None - self._b2_neutral_densities = None self._velocities_parallel = None self._velocities_radial = None self._velocities_toroidal = None self._velocities_cartesian = None - self._inside_mesh = None self._total_rad = None self._b_field_vectors = None self._b_field_vectors_cartesian = None - self._parallel_velocities = None - self._radial_velocities = None self._eirene_model = None self._b2_model = None self._eirene = None + @property + def mesh(self): + """ + SOLPSMesh instance. + :return: + """ + return self._mesh + + @property + def species_list(self): + """ + Tuple of species elements in the form (Element/Isotope, charge). + :return: + """ + return self._species_list + @property def electron_temperature(self): """ @@ -94,6 +98,12 @@ def electron_temperature(self): """ return self._electron_temperature + @electron_temperature.setter + def electron_temperature(self, value): + _check_array("electron_temperature", value, (self.mesh.nx, self.mesh.ny)) + + self._electron_temperature = value + @property def electron_density(self): """ @@ -102,13 +112,11 @@ def electron_density(self): """ return self._electron_density - @property - def species_list(self): - """ - Text list of species elements present in the simulation. - :return: - """ - return self._species_list + @electron_density.setter + def electron_density(self, value): + _check_array("electron_density", value, (self.mesh.nx, self.mesh.ny)) + + self._electron_density = value @property def species_density(self): @@ -118,13 +126,25 @@ def species_density(self): """ return self._species_density + @species_density.setter + def species_density(self, value): + _check_array("species_density", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) + + self._species_density = value + @property def radial_particle_flux(self): """ Blah :return: """ - return self._rad_par_flux + return self._radial_particle_flux + + @radial_particle_flux.setter + def radial_particle_flux(self, value): + _check_array("radial_particle_flux", value, (self.mesh.nx, self.mesh.ny - 1, len(self.species_list))) + + self._radial_particle_flux = value @property def radial_area(self): @@ -134,37 +154,59 @@ def radial_area(self): """ return self._radial_area - @property - def b2_neutral_densities(self): - """ - Neutral atom densities from B2 - :return: - """ - return self._b2_neutral_densities + @radial_area.setter + def radial_area(self, value): + _check_array("radial_area", value, (self.mesh.nx, self.mesh.ny - 1)) + + self._radial_area = value @property def velocities_parallel(self): return self._velocities_parallel + @velocities_parallel.setter + def velocities_parallel(self, value): + _check_array("velocities_parallel", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) + + self._velocities_parallel = value + @property def velocities_radial(self): return self._velocities_radial + @velocities_radial.setter + def velocities_radial(self, value): + _check_array("velocities_radial", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) + + self._velocities_radial = value + @property def velocities_toroidal(self): return self._velocities_toroidal + @velocities_toroidal.setter + def velocities_toroidal(self, value): + _check_array("velocities_toroidal", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) + + self._velocities_toroidal = value + @property def velocities_cartesian(self): return self._velocities_cartesian + @velocities_cartesian.setter + def velocities_cartesian(self, value): + _check_array("velocities_cartesian", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) + + self._velocities_cartesian = value + @property def inside_volume_mesh(self): """ Function3D for testing if point p is inside the simulation mesh. """ if self._inside_mesh is None: - raise RuntimeError("Inside mesh test not available for this simulation") + raise RuntimeError("Inside mesh test not available for this simulation.") else: return self._inside_mesh @@ -178,10 +220,17 @@ def total_radiation(self): and 'RQBRM'. Final output is in W/str? """ if self._total_rad is None: - raise RuntimeError("Total radiation not available for this simulation") + raise RuntimeError("Total radiation not available for this simulation.") else: return self._total_rad + @total_radiation.setter + def total_radiation(self, value): + _check_array("total_radiation", value, (self.mesh.nx, self.mesh.ny)) + + self._total_rad = value + + # TODO: decide is this a 2D or 3D interface? @property def total_radiation_volume(self): @@ -200,46 +249,38 @@ def total_radiation_volume(self): # return AxisymmetricMapper(radiation_mesh_2d) return radiation_mesh_2d - @property - def parallel_velocities(self): - """ - Plasma velocity field at each mesh cell. Equivalent to 'UA' in SOLPS. - """ - if self._parallel_velocities is None: - raise RuntimeError("Parallel velocities not available for this simulation") - else: - return self._parallel_velocities - - @property - def radial_velocities(self): - """ - Calculated radial velocity components for each species. - """ - if self._parallel_velocities is None: - raise RuntimeError("Radial velocities not available for this simulation") - else: - return self._radial_velocities - @property def b_field(self): """ - Magnetic B field at each mesh cell in mesh cell coordinates (b_parallel, b_radial b_toroidal). + Magnetic B field at each mesh cell in mesh cell coordinates (b_parallel, b_radial, b_toroidal). """ if self._b_field_vectors is None: - raise RuntimeError("Magnetic field not available for this simulation") + raise RuntimeError("Magnetic field not available for this simulation.") else: return self._b_field_vectors + @b_field.setter + def b_field(self, value): + _check_array("b_field", value, (self.mesh.nx, self.mesh.ny, 3)) + + self._b_field_vectors = value + @property def b_field_cartesian(self): """ Magnetic B field at each mesh cell in cartesian coordinates (Bx, By, Bz). """ if self._b_field_vectors_cartesian is None: - raise RuntimeError("Magnetic field not available for this simulation") + raise RuntimeError("Magnetic field not available for this simulation.") else: return self._b_field_vectors_cartesian + @b_field_cartesian.setter + def b_field_cartesian(self, value): + _check_array("b_field_cartesian", value, (self.mesh.nx, self.mesh.ny, 3)) + + self._b_field_vectors_cartesian = value + @property def eirene_simulation(self): """ @@ -248,10 +289,17 @@ def eirene_simulation(self): :rtype: Eirene """ if self._eirene is None: - raise RuntimeError("EIRENE simulation data not available for this SOLPS simulation") + raise RuntimeError("EIRENE simulation data not available for this SOLPS simulation.") else: return self._eirene + @eirene_simulation.setter + def eirene_simulation(self, value): + if not isinstance(value, Eirene): + raise ValueError('Attribute "eirene_simulation" must be an Eirene instance.') + + self._eirene = value + def __getstate__(self): state = { 'mesh': self.mesh.__getstate__(), @@ -259,9 +307,8 @@ def __getstate__(self): 'electron_density': self._electron_density, 'species_list': self._species_list, 'species_density': self._species_density, - 'rad_par_flux': self._rad_par_flux, + 'radial_particle_flux': self._radial_particle_flux, 'radial_area': self._radial_area, - 'b2_neutral_densities': self._b2_neutral_densities, 'velocities_parallel': self._velocities_parallel, 'velocities_radial': self._velocities_radial, 'velocities_toroidal': self._velocities_toroidal, @@ -270,8 +317,6 @@ def __getstate__(self): 'total_rad': self._total_rad, 'b_field_vectors': self._b_field_vectors, 'b_field_vectors_cartesian': self._b_field_vectors_cartesian, - 'parallel_velocities': self._parallel_velocities, - 'radial_velocities': self._radial_velocities, 'eirene_model': self._eirene_model, 'b2_model': self._b2_model, 'eirene': self._eirene @@ -284,9 +329,8 @@ def __setstate__(self, state): self._electron_density = state['electron_density'] self._species_list = state['species_list'] self._species_density = state['species_density'] - self._rad_par_flux = state['rad_par_flux'] + self._radial_particle_flux = state['radial_particle_flux'] self._radial_area = state['radial_area'] - self._b2_neutral_densities = state['b2_neutral_densities'] self._velocities_parallel = state['velocities_parallel'] self._velocities_radial = state['velocities_radial'] self._velocities_toroidal = state['velocities_toroidal'] @@ -295,8 +339,6 @@ def __setstate__(self, state): self._total_rad = state['total_rad'] self._b_field_vectors = state['b_field_vectors'] self._b_field_vectors_cartesian = state['b_field_vectors_cartesian'] - self._parallel_velocities = state['parallel_velocities'] - self._radial_velocities = state['radial_velocities'] self._eirene_model = state['eirene_model'] self._b2_model = state['b2_model'] self._eirene = state['eirene'] @@ -463,45 +505,35 @@ def create_plasma(self, parent=None, transform=None, name=None): tri_index_lookup = self.mesh.triangle_index_lookup tri_to_grid = self.mesh.triangle_to_grid_map - if isinstance(self._b_field_vectors, np.ndarray): - plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self._b_field_vectors_cartesian) - else: + try: + plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.b_field_cartesian) + except RuntimeError: print('Warning! No magnetic field data available for this simulation.') # Create electron species - triangle_data = _map_data_onto_triangles(self._electron_temperature) + triangle_data = _map_data_onto_triangles(self.electron_temperature) electron_te_interp = Discrete2DMesh(mesh.vertex_coords, mesh.triangles, triangle_data, limit=False) electron_temp = AxisymmetricMapper(electron_te_interp) - triangle_data = _map_data_onto_triangles(self._electron_density) + triangle_data = _map_data_onto_triangles(self.electron_density) electron_ne_interp = Discrete2DMesh.instance(electron_te_interp, triangle_data) electron_dens = AxisymmetricMapper(electron_ne_interp) electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) plasma.electron_distribution = Maxwellian(electron_dens, electron_temp, electron_velocity, electron_mass) - if not isinstance(self.velocities_cartesian, np.ndarray): + if self.velocities_cartesian is None: print('Warning! No velocity field data available for this simulation.') - b2_neutral_i = 0 # counter for B2 neutrals for k, sp in enumerate(self.species_list): - # Identify the species based on its symbol - symbol, charge = re.match(_SPECIES_REGEX, sp).groups() - charge = int(charge) - species_type = _species_symbol_map[symbol] + species_type = sp[0] + charge = sp[1] - # If neutral and B" atomic density available, use B2 density, otherwise use fluid species density. - if isinstance(self.b2_neutral_densities, np.ndarray) and charge == 0: - species_dens_data = self.b2_neutral_densities[:, :, b2_neutral_i] - b2_neutral_i += 1 - else: - species_dens_data = self.species_density[:, :, k] - - triangle_data = _map_data_onto_triangles(species_dens_data) + triangle_data = _map_data_onto_triangles(self.species_density[:, :, k]) dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, species_dens_data) + # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, self.species_density[:, :, k]) # Create the velocity vector lookup function - if isinstance(self.velocities_cartesian, np.ndarray): + if self.velocities_cartesian is not None: velocity = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.velocities_cartesian[:, :, k, :]) else: velocity = lambda x, y, z: Vector3D(0, 0, 0) @@ -534,3 +566,21 @@ def _map_data_onto_triangles(solps_dataset): tri_index += 1 return triangle_data + + +def _check_array(name, value, shape): + if not isinstance(value, np.ndarray): + raise ValueError('Attribute "%s" must be a numpy.ndarray' % name) + if value.shape != shape: + raise ValueError('Shape of "%s": %s mismatch the shape of SOLPS grid: %s.' % (name, value.shape, shape)) + + +def prefer_element(isotope): + """ + Return Element instance, if the element of this isotope has the same mass number. + """ + el_mass_number = int(round(isotope.element.atomic_weight)) + if el_mass_number == isotope.mass_number: + return isotope.element + + return isotope From 9006823c60948cd7ef2fb95d789ee44852d48763 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sat, 5 Sep 2020 02:57:39 +0300 Subject: [PATCH 02/25] Added ion and nuetral atom temperatures. --- cherab/solps/formats/balance.py | 3 + cherab/solps/formats/mdsplus.py | 44 +++++++-------- cherab/solps/formats/raw_simulation_files.py | 5 +- cherab/solps/solps_plasma.py | 58 ++++++++++++++++++-- 4 files changed, 82 insertions(+), 28 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 045496b..51cdd79 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -86,6 +86,9 @@ def load_solps_from_balance(balance_filename): sim.electron_temperature = fhandle.variables['te'].data.copy() / el_charge sim.electron_density = fhandle.variables['ne'].data.copy() + # Load ion temperature + sim.ion_temperature = fhandle.variables['ti'].data.copy() / el_charge + tmp = fhandle.variables['na'].data.copy() tmp = np.moveaxis(tmp, 0, -1) sim.species_density = tmp diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index e728ae1..9b289ba 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -17,16 +17,12 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -import re import numpy as np from raysect.core import Point2D -from cherab.core.atomic.elements import lookup_element, lookup_isotope +from cherab.core.atomic.elements import lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation - - -_SPECIES_REGEX = '([a-zA-z]+)\+?([0-9]+)' +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element # TODO: violates interface of SOLPSSimulation.... puts numpy arrays in the object where they should be function2D @@ -49,23 +45,16 @@ def load_solps_from_mdsplus(mds_server, ref_number): mesh = load_mesh_from_mdsplus(conn) # Load each plasma species - # Master list of species, e.g. ['D0', 'D+1', 'C0', 'C+1', ... - master_list = conn.get('\SOLPS::TOP.IDENT.SPECIES').data() - try: - master_list = master_list.decode('UTF-8').split() - except AttributeError: # Already a string - master_list = master_list.split() + ns = conn.get('\SOLPS::TOP.IDENT.NS').data() # Number of species + zn = conn.get('\SOLPS::TOP.SNAPSHOT.GRID.ZN').data().astype(np.int) # Nuclear charge + am = np.round(conn.get('\SOLPS::TOP.SNAPSHOT.GRID.AM').data()).astype(np.int) # Atomic mass number + charge = conn.get('\SOLPS::TOP.SNAPSHOT.GRID.ZA').data().astype(np.int) # Ionisation/charge species_list = [] - for sp in master_list: - # Only hydrogen isotopes (D, T) are supported in TOP.IDENT.SPECIES for now (no He3, C13, etc.), - # so re.match should be safe. - symbol, charge = re.match(_SPECIES_REGEX, sp).groups() - try: - species = lookup_element(symbol) - except ValueError: - species = lookup_isotope(symbol) - species_list.append((species, int(charge))) + for i in range(ns): + isotope = lookup_isotope(zn[i], number=am[i]) + species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same + species_list.append((species, charge[i])) sim = SOLPSSimulation(mesh, species_list) ni = mesh.nx @@ -99,6 +88,9 @@ def load_solps_from_mdsplus(mds_server, ref_number): sim.electron_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1) # (32, 98) => (98, 32) sim.electron_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NE').data(), 0, 1) # (32, 98) => (98, 32) + # Load ion temperature + sim.ion_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TI').data(), 0, 1) + # Load species sim.species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) sim.radial_particle_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) # radial particle flux @@ -117,6 +109,12 @@ def load_solps_from_mdsplus(mds_server, ref_number): sim.species_density[:, :, k] = neutral_densities[:, :, neutral_i] neutral_i += 1 + # Load the neutral atom temperature from Eirene if available + tab2 = conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data() + if isinstance(tab2, np.ndarray): + sim.neutral_temperature = np.swapaxes(tab2, 0, 2) + + # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. sim.velocities_parallel = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) sim.velocities_radial = np.zeros((ni, nj, len(sim.species_list))) sim.velocities_toroidal = np.zeros((ni, nj, len(sim.species_list))) @@ -126,8 +124,8 @@ def load_solps_from_mdsplus(mds_server, ref_number): # Calculate the species' velocity distribution # # calculate field component ratios for velocity conversion - bplane = np.sqrt(bparallel**2 + btoroidal**2) - parallel_to_toroidal_ratio = bparallel * btoroidal / (bplane**2) + bplane2 = bparallel**2 + btoroidal**2 + parallel_to_toroidal_ratio = bparallel * btoroidal / bplane2 # Calculate toroidal and radial velocity components sim.velocities_toroidal = sim.velocities_parallel * parallel_to_toroidal_ratio[:, :, None] diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 31b9494..4b38cce 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -85,9 +85,12 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.electron_temperature = mesh_data_dict['te'] / elementary_charge sim.electron_density = mesh_data_dict['ne'] + # Load ion temperature + sim.ion_temperature = mesh_data_dict['ti'] / elementary_charge + sim.species_density = mesh_data_dict['na'] - # Load total radiated power from EIRENE output file + # Load additional data from EIRENE output file eirene = load_fort44_file(eirene_fort44_file, debug=debug) sim.eirene_simulation = eirene diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 4cb69c8..2dd2872 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -60,6 +60,8 @@ def __init__(self, mesh, species_list): self._electron_temperature = None self._electron_density = None + self._ion_temperature = None + self._neutral_temperature = None self._species_density = None self._radial_particle_flux = None self._radial_area = None @@ -104,6 +106,35 @@ def electron_temperature(self, value): self._electron_temperature = value + @property + def ion_temperature(self): + """ + Simulated ion temperatures at each mesh cell. + :return: + """ + return self._ion_temperature + + @ion_temperature.setter + def ion_temperature(self, value): + _check_array("ion_temperature", value, (self.mesh.nx, self.mesh.ny)) + + self._ion_temperature = value + + @property + def neutral_temperature(self): + """ + Array of neutral atom (effective) temperature at each mesh cell. + :return: + """ + return self._neutral_temperature + + @neutral_temperature.setter + def neutral_temperature(self, value): + num_neutrals = len([sp for sp in self.species_list if sp[1] == 0]) + _check_array("neutral_temperature", value, (self.mesh.nx, self.mesh.ny, num_neutrals)) + + self._neutral_temperature = value + @property def electron_density(self): """ @@ -230,7 +261,6 @@ def total_radiation(self, value): self._total_rad = value - # TODO: decide is this a 2D or 3D interface? @property def total_radiation_volume(self): @@ -304,6 +334,8 @@ def __getstate__(self): state = { 'mesh': self.mesh.__getstate__(), 'electron_temperature': self._electron_temperature, + 'ion_temperature': self._ion_temperature, + 'neutral_temperature': self._neutral_temperature, 'electron_density': self._electron_density, 'species_list': self._species_list, 'species_density': self._species_density, @@ -326,6 +358,8 @@ def __getstate__(self): def __setstate__(self, state): self.mesh = SOLPSMesh(**state['mesh']) self._electron_temperature = state['electron_temperature'] + self._ion_temperature = state['ion_temperature'] + self._neutral_temperature = state['neutral_temperature'] self._electron_density = state['electron_density'] self._species_list = state['species_list'] self._species_density = state['species_density'] @@ -515,14 +549,21 @@ def create_plasma(self, parent=None, transform=None, name=None): electron_te_interp = Discrete2DMesh(mesh.vertex_coords, mesh.triangles, triangle_data, limit=False) electron_temp = AxisymmetricMapper(electron_te_interp) triangle_data = _map_data_onto_triangles(self.electron_density) - electron_ne_interp = Discrete2DMesh.instance(electron_te_interp, triangle_data) - electron_dens = AxisymmetricMapper(electron_ne_interp) + electron_dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) plasma.electron_distribution = Maxwellian(electron_dens, electron_temp, electron_velocity, electron_mass) + # Ion temperature + triangle_data = _map_data_onto_triangles(self.ion_temperature) + ion_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) + if self.velocities_cartesian is None: print('Warning! No velocity field data available for this simulation.') + if self.neutral_temperature is None: + print('Warning! No neutral atom temperature data available for this simulation.') + + neutral_i = 0 # neutrals count for k, sp in enumerate(self.species_list): species_type = sp[0] @@ -530,6 +571,7 @@ def create_plasma(self, parent=None, transform=None, name=None): triangle_data = _map_data_onto_triangles(self.species_density[:, :, k]) dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) + # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, self.species_density[:, :, k]) # Create the velocity vector lookup function @@ -538,7 +580,15 @@ def create_plasma(self, parent=None, transform=None, name=None): else: velocity = lambda x, y, z: Vector3D(0, 0, 0) - distribution = Maxwellian(dens, electron_temp, velocity, species_type.atomic_weight * atomic_mass) + if charge or self.neutral_temperature is None: # ions or neutral atoms (neutral temperature is not available) + distribution = Maxwellian(dens, ion_temp, velocity, species_type.atomic_weight * atomic_mass) + + else: # neutral atoms with neutral temperature + triangle_data = _map_data_onto_triangles(self.neutral_temperature[:, :, neutral_i]) + neutral_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) + distribution = Maxwellian(dens, neutral_temp, velocity, species_type.atomic_weight * atomic_mass) + neutral_i += 1 + plasma.composition.add(Species(species_type, charge, distribution)) return plasma From 1e41cd4d30248bb73c4c05eb337563ca3f269ffa Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sun, 6 Sep 2020 00:56:53 +0300 Subject: [PATCH 03/25] Many fixes and improvements in SOLPSMesh class --- cherab/solps/formats/mdsplus.py | 61 ++------- cherab/solps/formats/raw_pickle.py | 2 +- cherab/solps/mesh_geometry.py | 211 ++++++++++++++++++----------- cherab/solps/solps_plasma.py | 8 +- 4 files changed, 148 insertions(+), 134 deletions(-) diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index 9b289ba..c463784 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -69,13 +69,10 @@ def load_solps_from_mdsplus(mds_server, ref_number): bradial = b_field_vectors[:, :, 1] btoroidal = b_field_vectors[:, :, 2] - vec_x = np.vectorize(lambda obj: obj.x) - vec_y = np.vectorize(lambda obj: obj.y) - - pvx = vec_x(mesh.poloidal_grid_basis[:, :, 0]) # x-coordinate of parallel basis vector - pvy = vec_y(mesh.poloidal_grid_basis[:, :, 0]) # y-coordinate of parallel basis vector - rvx = vec_x(mesh.poloidal_grid_basis[:, :, 1]) # x-coordinate of radial basis vector - rvy = vec_y(mesh.poloidal_grid_basis[:, :, 1]) # y-coordinate of radial basis vector + pvx = mesh.parallel_basis_vector[:, :, 0] # x-coordinate of parallel basis vector + pvy = mesh.parallel_basis_vector[:, :, 1] # y-coordinate of parallel basis vector + rvx = mesh.radial_basis_vector[:, :, 0] # x-coordinate of radial basis vector + rvy = mesh.radial_basis_vector[:, :, 1] # y-coordinate of radial basis vector b_field_vectors_cartesian[:, :, 0] = pvx * bparallel + rvx * bradial # component of B along poloidal x b_field_vectors_cartesian[:, :, 2] = pvy * bparallel + rvy * bradial # component of B along poloidal y @@ -145,7 +142,6 @@ def load_solps_from_mdsplus(mds_server, ref_number): #################### # Integrated power # - vol = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.VOL').data(), 0, 1) # TODO - this should be a mesh property linerad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQRAD').data(), 0, 2) linerad = np.sum(linerad, axis=2) brmrad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data(), 0, 2) @@ -159,7 +155,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): else: neurad = np.zeros(brmrad.shape) - sim.total_radiation = (linerad + brmrad + neurad) / vol + sim.total_radiation = (linerad + brmrad + neurad) / mesh.vol return sim @@ -186,49 +182,8 @@ def load_mesh_from_mdsplus(mds_connection): ############################# # add the vessel geometry - mesh.vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data() - - # Load the centre points of the grid cells. - cr = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:CR').data(), 0, 1) - cz = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:CZ').data(), 0, 1) - mesh._cr = cr - mesh._cz = cz - - # Load cell basis vectors - nx = mesh.nx - ny = mesh.ny - - cell_poloidal_basis = np.empty((nx, ny, 2), dtype=object) - for i in range(nx): - for j in range(ny): - - # Work out cell's 2D parallel vector in the poloidal plane - if i == nx - 1: - # Special case for end of array, repeat previous calculation. - # This is because I don't have access to the gaurd cells. - xp_x = cr[i, j] - cr[i - 1, j] - xp_y = cz[i, j] - cz[i - 1, j] - norm = np.sqrt(xp_x**2 + xp_y**2) - cell_poloidal_basis[i, j, 0] = Point2D(xp_x / norm, xp_y / norm) - else: - xp_x = cr[i + 1, j] - cr[i, j] - xp_y = cz[i + 1, j] - cz[i, j] - norm = np.sqrt(xp_x**2 + xp_y**2) - cell_poloidal_basis[i, j, 0] = Point2D(xp_x / norm, xp_y / norm) - - # Work out cell's 2D radial vector in the poloidal plane - if j == ny - 1: - # Special case for end of array, repeat previous calculation. - yr_x = cr[i, j] - cr[i, j - 1] - yr_y = cz[i, j] - cz[i, j - 1] - norm = np.sqrt(yr_x**2 + yr_y**2) - cell_poloidal_basis[i, j, 1] = Point2D(yr_x / norm, yr_y / norm) - else: - yr_x = cr[i, j + 1] - cr[i, j] - yr_y = cz[i, j + 1] - cz[i, j] - norm = np.sqrt(yr_x**2 + yr_y**2) - cell_poloidal_basis[i, j, 1] = Point2D(yr_x / norm, yr_y / norm) - - mesh._poloidal_grid_basis = cell_poloidal_basis + vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data() + if isinstance(vessel, np.ndarray): + mesh.vessel = vessel return mesh diff --git a/cherab/solps/formats/raw_pickle.py b/cherab/solps/formats/raw_pickle.py index b4a99aa..6b33fa2 100644 --- a/cherab/solps/formats/raw_pickle.py +++ b/cherab/solps/formats/raw_pickle.py @@ -27,7 +27,7 @@ def load_solps_from_pickle(filename): file_handle = open(filename, 'rb') state = pickle.load(file_handle) mesh = SOLPSMesh(state['mesh']['cr_r'], state['mesh']['cr_z'], state['mesh']['vol']) - simulation = SOLPSSimulation(mesh) + simulation = SOLPSSimulation(mesh, state['species_list']) simulation.__setstate__(state) file_handle.close() diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index 77d117a..f4edd88 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -18,7 +18,7 @@ # under the Licence. # External imports -from collections import namedtuple +# from collections import namedtuple import numpy as np import matplotlib.pyplot as plt @@ -26,7 +26,7 @@ from matplotlib.collections import PatchCollection from raysect.core.math.function.float import Discrete2DMesh -INFINITY = 1E99 +# INFINITY = 1E99 class SOLPSMesh: @@ -40,80 +40,87 @@ class SOLPSMesh: triangle vertices. Therefore, each SOLPS rectangular cell is split into two triangular cells. The data points are later interpolated onto the vertex points. - :param ndarray cr_r: Array of cell vertex r coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). - :param ndarray cr_z: Array of cell vertex z coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). + :param ndarray r: Array of cell vertex r coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). + :param ndarray z: Array of cell vertex z coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). :param ndarray vol: Array of cell volumes. Example shape is (98 x 32). """ - def __init__(self, cr_r, cr_z, vol): + def __init__(self, r, z, vol): - self._cr = None - self._cz = None - self._poloidal_grid_basis = None + if r.shape != z.shape: + raise ValueError('Shape of r array: %s mismatch the shape of z array: %s.' % (r.shape, z.shape)) - nx = cr_r.shape[0] - ny = cr_r.shape[1] - self._nx = nx - self._ny = ny + if vol.shape != r.shape[:-1]: + raise ValueError('Shape of vol array: %s mismatch the grid dimentions: %s.' % (vol.shape, r.shape[:-1])) - self._r = cr_r - self._z = cr_z + self._cr = r.sum(2) / 4. + self._cz = z.sum(2) / 4. + + self._nx = r.shape[0] + self._ny = r.shape[1] + + self._r = r + self._z = z self._vol = vol - # Iterate through the arrays from MDS plus to pull out unique vertices - unique_vertices = {} - vertex_id = 0 - for i in range(nx): - for j in range(ny): - for k in range(4): - vertex = (cr_r[i, j, k], cr_z[i, j, k]) - try: - unique_vertices[vertex] - except KeyError: - unique_vertices[vertex] = vertex_id - vertex_id += 1 - - # Load these unique vertices into a numpy array for later use in Raysect's mesh interpolator object. - self.num_vertices = len(unique_vertices) - self.vertex_coords = np.zeros((self.num_vertices, 2), dtype=np.float64) - for vertex, vertex_id in unique_vertices.items(): - self.vertex_coords[vertex_id, :] = vertex + self.vessel = None + + # Calculating parallel basis vector + self._parallel_basis_vector = np.zeros((self._nx, self._ny, 2)) + vec_r = r[:, :, 1] - r[:, :, 0] + r[:, :, 3] - r[:, :, 2] + vec_z = z[:, :, 1] - z[:, :, 0] + z[:, :, 3] - z[:, :, 2] + vec_magn = np.sqrt(vec_r**2 + vec_z**2) + self._parallel_basis_vector[:, :, 0] = vec_r / vec_magn + self._parallel_basis_vector[:, :, 1] = vec_z / vec_magn + + # Calculating radial basis vector + self._radial_basis_vector = np.zeros((self._nx, self._ny, 2)) + vec_r = r[:, :, 2] - r[:, :, 0] + r[:, :, 3] - r[:, :, 1] + vec_z = z[:, :, 2] - z[:, :, 0] + z[:, :, 3] - z[:, :, 1] + vec_magn = np.sqrt(vec_r**2 + vec_z**2) + self._radial_basis_vector[:, :, 0] = vec_r / vec_magn + self._radial_basis_vector[:, :, 1] = vec_z / vec_magn + + # Test for basis vector calculation + # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._radial_basis_vector[:, 0, 0], self._radial_basis_vector[:, 0, 1], color='k') + # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._parallel_basis_vector[:, 0, 0], self._parallel_basis_vector[:, 0, 1], color='r') + # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._radial_basis_vector[:, -1, 0], self._radial_basis_vector[:, -1, 1], color='k') + # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._parallel_basis_vector[:, -1, 0], self._parallel_basis_vector[:, -1, 1], color='r') + # plt.gca().set_aspect('equal') + # plt.show() + + # Finding unique vertices + vertices = np.array([r.flatten(), z.flatten()]).T + self._vertex_coords, unique_vertices = np.unique(vertices, axis=0, return_inverse=True) + self._num_vertices = self._vertex_coords.shape[0] # Work out the extent of the mesh. - rmin = cr_r.flatten().min() - rmax = cr_r.flatten().max() - zmin = cr_z.flatten().min() - zmax = cr_z.flatten().max() - self.mesh_extent = {"minr": rmin, "maxr": rmax, "minz": zmin, "maxz": zmax} + self._mesh_extent = {"minr": r.min(), "maxr": r.max(), "minz": z.min(), "maxz": z.max()} # Number of triangles must be equal to number of rectangle centre points times 2. - self.num_tris = nx * ny * 2 - self.triangles = np.zeros((self.num_tris, 3), dtype=np.int32) - - self._triangle_to_grid_map = np.zeros((nx*ny*2, 2), dtype=np.int32) - tri_index = 0 - for i in range(nx): - for j in range(ny): - # Pull out the index number for each unique vertex in this rectangular cell. - # Unusual vertex indexing is based on SOLPS output, see Matlab code extract from David Moulton. - # cell_r = [r(i,j,1),r(i,j,3),r(i,j,4),r(i,j,2)]; - v1_id = unique_vertices[(cr_r[i, j, 0], cr_z[i, j, 0])] - v2_id = unique_vertices[(cr_r[i, j, 2], cr_z[i, j, 2])] - v3_id = unique_vertices[(cr_r[i, j, 3], cr_z[i, j, 3])] - v4_id = unique_vertices[(cr_r[i, j, 1], cr_z[i, j, 1])] - - # Split the quad cell into two triangular cells. - # Each triangle cell is mapped to the tuple ID (ix, iy) of its parent mesh cell. - self.triangles[tri_index, :] = (v1_id, v2_id, v3_id) - self._triangle_to_grid_map[tri_index, :] = (i, j) - tri_index += 1 - self.triangles[tri_index, :] = (v3_id, v4_id, v1_id) - self._triangle_to_grid_map[tri_index, :] = (i, j) - tri_index += 1 - - tri_indices = np.arange(self.num_tris, dtype=np.int32) - self._tri_index_loopup = Discrete2DMesh(self.vertex_coords, self.triangles, tri_indices) + self._num_tris = self._nx * self._ny * 2 + self._triangles = np.zeros((self._num_tris, 3), dtype=np.int32) + self._triangle_to_grid_map = np.zeros((self._num_tris, 2), dtype=np.int32) + + # Pull out the index number for each unique vertex in this rectangular cell. + # Unusual vertex indexing is based on SOLPS output, see Matlab code extract from David Moulton. + self._triangles[0::2, 0] = unique_vertices[0::4] + self._triangles[0::2, 1] = unique_vertices[2::4] + self._triangles[0::2, 2] = unique_vertices[3::4] + # Split the quad cell into two triangular cells. + self._triangles[1::2, 0] = unique_vertices[3::4] + self._triangles[1::2, 1] = unique_vertices[1::4] + self._triangles[1::2, 2] = unique_vertices[0::4] + + # Each triangle cell is mapped to the tuple ID (ix, iy) of its parent mesh cell. + xm, ym = np.meshgrid(np.arange(self._nx, dtype=np.int32), np.arange(self._ny, dtype=np.int32), indexing='ij') + self._triangle_to_grid_map[::2, 0] = xm.flatten() + self._triangle_to_grid_map[::2, 1] = ym.flatten() + self._triangle_to_grid_map[1::2, :] = self._triangle_to_grid_map[::2, :] + + tri_indices = np.arange(self._num_tris, dtype=np.int32) + self._tri_index_loopup = Discrete2DMesh(self._vertex_coords, self._triangles, tri_indices) @property def nx(self): @@ -135,15 +142,50 @@ def cz(self): """Z-coordinate of the cell centres.""" return self._cz + @property + def r(self): + """R-coordinates of the cell vertices.""" + return self._r + + @property + def z(self): + """Z-coordinate of the cell vertices.""" + return self._z + @property def vol(self): """Volume/area of each grid cell.""" return self._vol @property - def poloidal_grid_basis(self): + def vertex_coordinates(self): + """RZ-coordinates of unique vertices.""" + return self._vertex_coords + + @property + def num_vertices(self): + """Total number of unique vertices.""" + return self._num_vertices + + @property + def mesh_extent(self): + """Extent of the mesh. A dictionary with minr, maxr, minz and maxz keys.""" + return self._mesh_extent + + @property + def num_triangles(self): + """Total number of triangles (the number of cells doubled).""" + return self._num_tris + + @property + def triangles(self): + """Array of triangle vertex indices with (num_thiangles, 3) shape.""" + return self._triangles + + @property + def parallel_basis_vector(self): """ - Array of 2D basis vectors for grid cells. + Array of 2D parallel basis vectors for grid cells. For each cell there is a parallel and radial basis vector. @@ -151,9 +193,24 @@ def poloidal_grid_basis(self): bx = (p_x r_x) ( b_p ) by (p_y r_y) ( b_r ) - :return: ndarray with shape (nx, ny, 2) where the two basis vectors are [parallel, radial] respectively. + :return: ndarray with shape (nx, ny, 2). """ - return self._poloidal_grid_basis + return self._parallel_basis_vector + + @property + def radial_basis_vector(self): + """ + Array of 2D radial basis vectors for grid cells. + + For each cell there is a parallel and radial basis vector. + + Any vector on the poloidal grid can be converted to cartesian with the following transformation. + bx = (p_x r_x) ( b_p ) + by (p_y r_y) ( b_r ) + + :return: ndarray with shape (nx, ny, 2). + """ + return self._radial_basis_vector @property def triangle_to_grid_map(self): @@ -177,9 +234,9 @@ def triangle_index_lookup(self): def __getstate__(self): state = { - 'cr_r': self._r, - 'cr_z': self._z, - 'vol': self._vol, + 'r': self._r, + 'z': self._z, + 'vol': self._vol } return state @@ -190,15 +247,17 @@ def plot_mesh(self): fig, ax = plt.subplots() patches = [] for triangle in self.triangles: - vertices = self.vertex_coords[triangle] + vertices = self.vertex_coordinates[triangle] patches.append(Polygon(vertices, closed=True)) p = PatchCollection(patches, facecolors='none', edgecolors='b') ax.add_collection(p) ax.axis('equal') - return ax # Code for plotting vessel geometry if available - # for i in range(vessel.shape[0]): - # plt.plot([vessel[i, 0], vessel[i, 2]], [vessel[i, 1], vessel[i, 3]], 'k') - # for i in range(vessel.shape[0]): - # plt.plot([vessel[i, 0], vessel[i, 2]], [vessel[i, 1], vessel[i, 3]], 'or') + # if self.vessel is not None: + # for i in range(self.vessel.shape[0]): + # ax.plot([self.vessel[i, 0], self.vessel[i, 2]], [self.vessel[i, 1], self.vessel[i, 3]], 'k') + # for i in range(self.vessel.shape[0]): + # ax.plot([self.vessel[i, 0], self.vessel[i, 2]], [self.vessel[i, 1], self.vessel[i, 3]], 'or') + + return ax diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 2dd2872..5929215 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -50,8 +50,8 @@ def __init__(self, mesh, species_list): raise ValueError('Argument "mesh" must be a SOLPSMesh instance.') # Make Mesh Interpolator function for inside/outside mesh test. - inside_outside_data = np.ones(mesh.num_tris) - inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coords, mesh.triangles, inside_outside_data, limit=False)) + inside_outside_data = np.ones(mesh.num_triangles) + inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coordinates, mesh.triangles, inside_outside_data, limit=False)) self._inside_mesh = inside_outside if not len(species_list): @@ -275,7 +275,7 @@ def total_radiation_volume(self): """ mapped_radiation_data = _map_data_onto_triangles(self._total_rad) - radiation_mesh_2d = Discrete2DMesh(self.mesh.vertex_coords, self.mesh.triangles, mapped_radiation_data, limit=False) + radiation_mesh_2d = Discrete2DMesh(self.mesh.vertex_coordinates, self.mesh.triangles, mapped_radiation_data, limit=False) # return AxisymmetricMapper(radiation_mesh_2d) return radiation_mesh_2d @@ -546,7 +546,7 @@ def create_plasma(self, parent=None, transform=None, name=None): # Create electron species triangle_data = _map_data_onto_triangles(self.electron_temperature) - electron_te_interp = Discrete2DMesh(mesh.vertex_coords, mesh.triangles, triangle_data, limit=False) + electron_te_interp = Discrete2DMesh(mesh.vertex_coordinates, mesh.triangles, triangle_data, limit=False) electron_temp = AxisymmetricMapper(electron_te_interp) triangle_data = _map_data_onto_triangles(self.electron_density) electron_dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) From 687e991bc54bb367f676cf5ecd7839e04dbc47ea Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sun, 6 Sep 2020 09:11:11 +0300 Subject: [PATCH 04/25] Corrected mesh basis vector calculation --- cherab/solps/mesh_geometry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index f4edd88..5617ae6 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -68,16 +68,16 @@ def __init__(self, r, z, vol): # Calculating parallel basis vector self._parallel_basis_vector = np.zeros((self._nx, self._ny, 2)) - vec_r = r[:, :, 1] - r[:, :, 0] + r[:, :, 3] - r[:, :, 2] - vec_z = z[:, :, 1] - z[:, :, 0] + z[:, :, 3] - z[:, :, 2] + vec_r = r[:, :, 1] - r[:, :, 0] + vec_z = z[:, :, 1] - z[:, :, 0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) self._parallel_basis_vector[:, :, 0] = vec_r / vec_magn self._parallel_basis_vector[:, :, 1] = vec_z / vec_magn # Calculating radial basis vector self._radial_basis_vector = np.zeros((self._nx, self._ny, 2)) - vec_r = r[:, :, 2] - r[:, :, 0] + r[:, :, 3] - r[:, :, 1] - vec_z = z[:, :, 2] - z[:, :, 0] + z[:, :, 3] - z[:, :, 1] + vec_r = r[:, :, 2] - r[:, :, 0] + vec_z = z[:, :, 2] - z[:, :, 0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) self._radial_basis_vector[:, :, 0] = vec_r / vec_magn self._radial_basis_vector[:, :, 1] = vec_z / vec_magn From 5dcc0dd953da78d5c2dcbf2ce66224fc7b3b6857 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sun, 6 Sep 2020 18:14:35 +0300 Subject: [PATCH 05/25] Added poloidal <--> Cartesian conversion to SOLPSMesh. Added conversion on assignment to b_field and velocities in SOLPSSimulation. --- cherab/solps/formats/mdsplus.py | 42 ++++++---------------- cherab/solps/mesh_geometry.py | 34 ++++++++++++++++++ cherab/solps/solps_plasma.py | 64 ++++++++++++++++----------------- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index c463784..175be45 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -62,24 +62,8 @@ def load_solps_from_mdsplus(mds_server, ref_number): ########################## # Magnetic field vectors # - b_field_vectors = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2)[:, :, :3] - b_field_vectors_cartesian = np.zeros((ni, nj, 3)) - - bparallel = b_field_vectors[:, :, 0] - bradial = b_field_vectors[:, :, 1] - btoroidal = b_field_vectors[:, :, 2] - - pvx = mesh.parallel_basis_vector[:, :, 0] # x-coordinate of parallel basis vector - pvy = mesh.parallel_basis_vector[:, :, 1] # y-coordinate of parallel basis vector - rvx = mesh.radial_basis_vector[:, :, 0] # x-coordinate of radial basis vector - rvy = mesh.radial_basis_vector[:, :, 1] # y-coordinate of radial basis vector - - b_field_vectors_cartesian[:, :, 0] = pvx * bparallel + rvx * bradial # component of B along poloidal x - b_field_vectors_cartesian[:, :, 2] = pvy * bparallel + rvy * bradial # component of B along poloidal y - b_field_vectors_cartesian[:, :, 1] = btoroidal - - sim.b_field = b_field_vectors - sim.b_field_cartesian = b_field_vectors_cartesian + sim.b_field = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2)[:, :, :3] + # sim.b_field_cartesian is created authomatically # Load electron temperature and density sim.electron_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1) # (32, 98) => (98, 32) @@ -112,29 +96,23 @@ def load_solps_from_mdsplus(mds_server, ref_number): sim.neutral_temperature = np.swapaxes(tab2, 0, 2) # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. - sim.velocities_parallel = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) - sim.velocities_radial = np.zeros((ni, nj, len(sim.species_list))) - sim.velocities_toroidal = np.zeros((ni, nj, len(sim.species_list))) - sim.velocities_cartesian = np.zeros((ni, nj, len(sim.species_list), 3)) - + velocities = np.zeros((ni, nj, len(sim.species_list), 3)) + velocities[:, :, :, 0] = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) ################################################ # Calculate the species' velocity distribution # # calculate field component ratios for velocity conversion - bplane2 = bparallel**2 + btoroidal**2 - parallel_to_toroidal_ratio = bparallel * btoroidal / bplane2 + bplane2 = sim.b_field[:, :, 0]**2 + sim.b_field[:, :, 2]**2 + parallel_to_toroidal_ratio = sim.b_field[:, :, 0] * sim.b_field[:, :, 2] / bplane2 # Calculate toroidal and radial velocity components - sim.velocities_toroidal = sim.velocities_parallel * parallel_to_toroidal_ratio[:, :, None] + velocities[:, :, :, 2] = velocities[:, :, :, 0] * parallel_to_toroidal_ratio[:, :, None] for k, sp in enumerate(sim.species_list): i, j = np.where(sim.species_density[:, :-1, k] > 0) - sim.velocities_radial[i, j, k] = sim.radial_particle_flux[i, j, k] / sim.radial_area[i, j] / sim.species_density[i, j, k] - - # Convert velocities to cartesian coordinates - sim.velocities_cartesian[:, :, :, 0] = pvx[:, :, None] * sim.velocities_parallel + rvx[:, :, None] * sim.velocities_radial # component of v along poloidal x - sim.velocities_cartesian[:, :, :, 2] = pvy[:, :, None] * sim.velocities_parallel + rvy[:, :, None] * sim.velocities_radial # component of v along poloidal y - sim.velocities_cartesian[:, :, :, 1] = sim.velocities_toroidal + velocities[i, j, k, 1] = sim.radial_particle_flux[i, j, k] / sim.radial_area[i, j] / sim.species_density[i, j, k] + sim.velocities = velocities + # sim.velocities_cartesian is created authomatically ############################### # Load extra data from server # diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index 5617ae6..dd67662 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -82,6 +82,10 @@ def __init__(self, r, z, vol): self._radial_basis_vector[:, :, 0] = vec_r / vec_magn self._radial_basis_vector[:, :, 1] = vec_z / vec_magn + # For convertion from Cartesian to poloidal + self._inv_det = 1. / (self._parallel_basis_vector[:, :, 0] * self._radial_basis_vector[:, :, 1] - + self._parallel_basis_vector[:, :, 1] * self._radial_basis_vector[:, :, 0]) + # Test for basis vector calculation # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._radial_basis_vector[:, 0, 0], self._radial_basis_vector[:, 0, 1], color='k') # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._parallel_basis_vector[:, 0, 0], self._parallel_basis_vector[:, 0, 1], color='r') @@ -240,6 +244,36 @@ def __getstate__(self): } return state + def to_cartesian(self, vec_pol): + """ + Converts the 2D vector defined on mesh from poloidal to cartesian coordinates. + :param ndarray vec_pol: Array of 2D vector with with shape (nx, ny, 2). + [:, :, 0] - parallel component, [:, :, 1] - radial component + + :return: ndarray with shape (nx, ny, 2) + """ + vec_cart = np.zeros((self._nx, self._ny, 2)) + vec_cart[:, :, 0] = self._parallel_basis_vector[:, :, 0] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 0] * vec_pol[:, :, 1] + vec_cart[:, :, 1] = self._parallel_basis_vector[:, :, 1] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 1] * vec_pol[:, :, 1] + + return vec_cart + + def to_poloidal(self, vec_cart): + """ + Converts the 2D vector defined on mesh from cartesian to poloidal coordinates. + :param ndarray vector_on_mesh: Array of 2D vector with with shape (nx, ny, 2). + [:, :, 0] - R component, [:, :, 1] - Z component + + :return: ndarray with shape (nx, ny, 2) + """ + vec_pol = np.zeros((self._nx, self._ny, 2)) + vec_pol[:, :, 0] = self._inv_det * (self._radial_basis_vector[:, :, 1] * vec_cart[:, :, 0] - + self._radial_basis_vector[:, :, 0] * vec_cart[:, :, 1]) + vec_pol[:, :, 1] = self._inv_det * (self._parallel_basis_vector[:, :, 0] * vec_cart[:, :, 1] - + self._parallel_basis_vector[:, :, 1] * vec_cart[:, :, 0]) + + return vec_pol + def plot_mesh(self): """ Plot the mesh grid geometry to a matplotlib figure. diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 5929215..6f4b844 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -65,9 +65,7 @@ def __init__(self, mesh, species_list): self._species_density = None self._radial_particle_flux = None self._radial_area = None - self._velocities_parallel = None - self._velocities_radial = None - self._velocities_toroidal = None + self._velocities = None self._velocities_cartesian = None self._total_rad = None self._b_field_vectors = None @@ -192,34 +190,20 @@ def radial_area(self, value): self._radial_area = value @property - def velocities_parallel(self): - return self._velocities_parallel + def velocities(self): + return self._velocities_vectors - @velocities_parallel.setter - def velocities_parallel(self, value): - _check_array("velocities_parallel", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) + @velocities.setter + def velocities(self, value): + _check_array("velocities", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) - self._velocities_parallel = value + # Converting to cartesian system + self._velocities_cartesian = np.zeros(value.shape) + self._velocities_cartesian[:, :, :, 2] = value[:, :, :, 2] + for k in range(value.shape[2]): + self._velocities_cartesian[:, :, k, :2] = self.mesh.to_cartesian(value[:, :, k, :2]) - @property - def velocities_radial(self): - return self._velocities_radial - - @velocities_radial.setter - def velocities_radial(self, value): - _check_array("velocities_radial", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) - - self._velocities_radial = value - - @property - def velocities_toroidal(self): - return self._velocities_toroidal - - @velocities_toroidal.setter - def velocities_toroidal(self, value): - _check_array("velocities_toroidal", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) - - self._velocities_toroidal = value + self._velocities = value @property def velocities_cartesian(self): @@ -229,6 +213,12 @@ def velocities_cartesian(self): def velocities_cartesian(self, value): _check_array("velocities_cartesian", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) + # Converting to poloidal system + self._velocities = np.zeros(value.shape) + self._velocities[:, :, :, 2] = value[:, :, :, 2] + for k in range(value.shape[2]): + self._velocities[:, :, k, :2] = self.mesh.to_poloidal(value[:, :, k, :2]) + self._velocities_cartesian = value @property @@ -293,6 +283,11 @@ def b_field(self): def b_field(self, value): _check_array("b_field", value, (self.mesh.nx, self.mesh.ny, 3)) + # Converting to cartesian system + self._b_field_vectors_cartesian = np.zeros(value.shape) + self._b_field_vectors_cartesian[:, :, 2] = value[:, :, 2] + self._b_field_vectors_cartesian[:, :, :2] = self.mesh.to_cartesian(value[:, :, :2]) + self._b_field_vectors = value @property @@ -309,6 +304,11 @@ def b_field_cartesian(self): def b_field_cartesian(self, value): _check_array("b_field_cartesian", value, (self.mesh.nx, self.mesh.ny, 3)) + # Converting to poloidal system + self._b_field_vectors = np.zeros(value.shape) + self._b_field_vectors[:, :, 2] = value[:, :, 2] + self._b_field_vectors[:, :, :2] = self.mesh.to_poloidal(value[:, :, :2]) + self._b_field_vectors_cartesian = value @property @@ -341,9 +341,7 @@ def __getstate__(self): 'species_density': self._species_density, 'radial_particle_flux': self._radial_particle_flux, 'radial_area': self._radial_area, - 'velocities_parallel': self._velocities_parallel, - 'velocities_radial': self._velocities_radial, - 'velocities_toroidal': self._velocities_toroidal, + 'velocities': self._velocities, 'velocities_cartesian': self._velocities_cartesian, 'inside_mesh': self._inside_mesh, 'total_rad': self._total_rad, @@ -365,9 +363,7 @@ def __setstate__(self, state): self._species_density = state['species_density'] self._radial_particle_flux = state['radial_particle_flux'] self._radial_area = state['radial_area'] - self._velocities_parallel = state['velocities_parallel'] - self._velocities_radial = state['velocities_radial'] - self._velocities_toroidal = state['velocities_toroidal'] + self._velocities = state['velocities'] self._velocities_cartesian = state['velocities_cartesian'] self._inside_mesh = state['inside_mesh'] self._total_rad = state['total_rad'] From bdf99dac0163d47550cb7846acf7995d8208f0bc Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sun, 6 Sep 2020 19:18:55 +0300 Subject: [PATCH 06/25] Replaced nested loops in _map_data_onto_triangles with vector code. --- cherab/solps/solps_plasma.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 6f4b844..12e8ccf 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -598,18 +598,10 @@ def _map_data_onto_triangles(solps_dataset): :return: New 1D ndarray with shape (98*32*2) """ - solps_mesh_shape = solps_dataset.shape - triangle_data = np.zeros(solps_mesh_shape[0] * solps_mesh_shape[1] * 2, dtype=np.float64) - - tri_index = 0 - for i in range(solps_mesh_shape[0]): - for j in range(solps_mesh_shape[1]): - - # Same data - triangle_data[tri_index] = solps_dataset[i, j] - tri_index += 1 - triangle_data[tri_index] = solps_dataset[i, j] - tri_index += 1 + triangle_data = np.zeros(solps_dataset.size * 2, dtype=np.float64) + + triangle_data[::2] = solps_dataset.flatten() + triangle_data[1::2] = triangle_data[::2] return triangle_data From 800b87855fa2ef91eaae0b5effce2f9bfc1026c2 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Mon, 7 Sep 2020 02:01:07 +0300 Subject: [PATCH 07/25] Added magnetic files, velocity and neutral temperature reading to raw_simulation_files. Removed radial_particle_flux and radial_area attributes from SOLPSSimulation. --- cherab/solps/formats/mdsplus.py | 17 ++- cherab/solps/formats/raw_simulation_files.py | 111 ++++++++++++------- cherab/solps/solps_plasma.py | 34 ------ 3 files changed, 81 insertions(+), 81 deletions(-) diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index 175be45..a2f7fcc 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -24,6 +24,8 @@ from cherab.solps.mesh_geometry import SOLPSMesh from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element +from matplotlib import pyplot as plt + # TODO: violates interface of SOLPSSimulation.... puts numpy arrays in the object where they should be function2D def load_solps_from_mdsplus(mds_server, ref_number): @@ -72,10 +74,8 @@ def load_solps_from_mdsplus(mds_server, ref_number): # Load ion temperature sim.ion_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TI').data(), 0, 1) - # Load species + # Load species density sim.species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) - sim.radial_particle_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) # radial particle flux - sim.radial_area = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.SY').data(), 0, 1) # radial contact area # Load the neutral atom density from Eirene if available dab2 = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() @@ -98,6 +98,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. velocities = np.zeros((ni, nj, len(sim.species_list), 3)) velocities[:, :, :, 0] = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) + ################################################ # Calculate the species' velocity distribution # @@ -105,12 +106,16 @@ def load_solps_from_mdsplus(mds_server, ref_number): bplane2 = sim.b_field[:, :, 0]**2 + sim.b_field[:, :, 2]**2 parallel_to_toroidal_ratio = sim.b_field[:, :, 0] * sim.b_field[:, :, 2] / bplane2 - # Calculate toroidal and radial velocity components + # Calculate toroidal velocity components velocities[:, :, :, 2] = velocities[:, :, :, 0] * parallel_to_toroidal_ratio[:, :, None] + # Radial velocity is obtained from radial particle flux + radial_particle_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) # radial particle flux + radial_area = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.SY').data(), 0, 1) # radial contact area for k, sp in enumerate(sim.species_list): - i, j = np.where(sim.species_density[:, :-1, k] > 0) - velocities[i, j, k, 1] = sim.radial_particle_flux[i, j, k] / sim.radial_area[i, j] / sim.species_density[i, j, k] + i, j = np.where(sim.species_density[:, :-1, k] > 0) # radial_area array corresponds to [:, 1:] in mesh, so maybe [:, 1:, k] + velocities[i, j, k, 1] = radial_particle_flux[i, j, k] / radial_area[i, j] / sim.species_density[i, j, k] + sim.velocities = velocities # sim.velocities_cartesian is created authomatically diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 4b38cce..58c4bcf 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -39,30 +39,39 @@ def load_solps_from_raw_output(simulation_path, debug=False): Required files include: * mesh description file (b2fgmtry) * B2 plasma state (b2fstate) - * Eirene output file (fort.44) + * Eirene output file (fort.44), optional :param str simulation_path: String path to simulation directory. :rtype: SOLPSSimulation """ if not os.path.isdir(simulation_path): - RuntimeError("simulation_path must be a valid directory") + RuntimeError("Simulation_path must be a valid directory.") mesh_file_path = os.path.join(simulation_path, 'b2fgmtry') b2_state_file = os.path.join(simulation_path, 'b2fstate') eirene_fort44_file = os.path.join(simulation_path, "fort.44") if not os.path.isfile(mesh_file_path): - raise RuntimeError("No B2 b2fgmtry file found in SOLPS output directory") + raise RuntimeError("No B2 b2fgmtry file found in SOLPS output directory.") if not(os.path.isfile(b2_state_file)): - RuntimeError("No B2 b2fstate file found in SOLPS output directory") + RuntimeError("No B2 b2fstate file found in SOLPS output directory.") if not(os.path.isfile(eirene_fort44_file)): - RuntimeError("No EIRENE fort.44 file found in SOLPS output directory") + print("Warning! No EIRENE fort.44 file found in SOLPS output directory. Assuming B2.5 stand-alone simulation.") + b2_standalone = True + else: + # Load data for neutral species from EIRENE output file + eirene = load_fort44_file(eirene_fort44_file, debug=debug) + b2_standalone = False # Load SOLPS mesh geometry - mesh = load_mesh_from_files(mesh_file_path=mesh_file_path, debug=debug) + _, _, geom_data_dict = load_b2f_file(mesh_file_path, debug=debug) # geom_data_dict is needed also for magnetic field + + mesh = SOLPSMesh(geom_data_dict['crx'], geom_data_dict['cry'], geom_data_dict['vol']) + ni = mesh.nx + nj = mesh.ny header_dict, sim_info_dict, mesh_data_dict = load_b2f_file(b2_state_file, debug=debug) @@ -79,7 +88,9 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim = SOLPSSimulation(mesh, species_list) - # TODO: add code to load SOLPS velocities and magnetic field from files + # Load magnetic field + sim.b_field = geom_data_dict['bb'][:, :, :3] + # sim.b_field_cartesian is created authomatically # Load electron species sim.electron_temperature = mesh_data_dict['te'] / elementary_charge @@ -88,48 +99,66 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Load ion temperature sim.ion_temperature = mesh_data_dict['ti'] / elementary_charge + # Load species density sim.species_density = mesh_data_dict['na'] - # Load additional data from EIRENE output file - eirene = load_fort44_file(eirene_fort44_file, debug=debug) - sim.eirene_simulation = eirene + if not b2_standalone: + # Replacing B2 neutral densities with EIRENE ones + da_raw_data = eirene.da + neutral_i = 0 # counter for neutral atoms + for k, sp in enumerate(sim.species_list): + charge = sp[1] + if charge == 0: + sim.species_density[1:-1, 1:-1, k] = da_raw_data[:, :, neutral_i] + neutral_i += 1 - # Note EIRENE data grid is slightly smaller than SOLPS grid, for example (98, 38) => (96, 36) - # Need to pad EIRENE data to fit inside larger B2 array - nx = mesh.nx - ny = mesh.ny + # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. + velocities = np.zeros((ni, nj, len(sim.species_list), 3)) + velocities[:, :, :, 0] = mesh_data_dict['ua'] - # Replacing B2 neutral densities with EIRENE ones - da_raw_data = eirene.da - neutral_i = 0 # counter for neutral atoms - for k, sp in enumerate(sim.species_list): - charge = sp[1] - if charge == 0: - sim.species_density[1:-1, 1:-1, k] = da_raw_data[:, :, neutral_i] - neutral_i += 1 + ################################################ + # Calculate the species' velocity distribution # - # Obtaining total radiation - eradt_raw_data = eirene.eradt.sum(2) - sim.total_radiation = np.zeros((nx, ny)) - sim.total_radiation[1:-1, 1:-1] = eradt_raw_data + # calculate field component ratios for velocity conversion + bplane2 = sim.b_field[:, :, 0]**2 + sim.b_field[:, :, 2]**2 + parallel_to_toroidal_ratio = sim.b_field[:, :, 0] * sim.b_field[:, :, 2] / bplane2 - return sim + # Calculate toroidal velocity component + velocities[:, :, :, 2] = velocities[:, :, :, 0] * parallel_to_toroidal_ratio[:, :, None] + # Radial velocity is obtained from radial particle flux + radial_particle_flux = mesh_data_dict['fna'][:, :, 1::2] -def load_mesh_from_files(mesh_file_path, debug=False): - """ - Load SOLPS grid description from B2 Eirene output file. + vec_r = mesh.r[:, :, 1] - mesh.r[:, :, 0] + vec_z = mesh.z[:, :, 1] - mesh.z[:, :, 0] + radial_area = np.pi * (mesh.r[:, :, 1] + mesh.r[:, :, 0]) * np.sqrt(vec_r**2 + vec_z**2) - :param str filepath: full path for B2 eirene mesh description file - :param bool debug: flag for displaying textual debugging information. - :return: tuple of dictionaries. First is the header information such as the version, label, grid size, etc. - Second dictionary has a ndarray for each piece of data found in the file. - """ - _, _, geom_data_dict = load_b2f_file(mesh_file_path, debug=debug) + for k, sp in enumerate(sim.species_list): + i, j = np.where(sim.species_density[:, :, k] > 0) + velocities[i, j, k, 1] = radial_particle_flux[i, j, k] / radial_area[i, j] / sim.species_density[i, j, k] - cr_x = geom_data_dict['crx'] - cr_z = geom_data_dict['cry'] - vol = geom_data_dict['vol'] + sim.velocities = velocities + # sim.velocities_cartesian is created authomatically - # build mesh object - return SOLPSMesh(cr_x, cr_z, vol) + if not b2_standalone: + # Note EIRENE data grid is slightly smaller than SOLPS grid, for example (98, 38) => (96, 36) + # Need to pad EIRENE data to fit inside larger B2 array + + # Obtaining neutral temperatures + ta = np.zeros((ni, nj, eirene.ta.shape[2])) + ta[1:-1, 1:-1, :] = eirene.ta + for i in (0, -1): + ta[i, 1:-1, :] = eirene.ta[i, :, :] + ta[1:-1, i, :] = eirene.ta[:, i, :] + for i, j in ((0, 0), (0, -1), (-1, 0), (-1, -1)): + ta[i, j, :] = eirene.ta[i, j, :] + sim.neutral_temperature = ta / elementary_charge + + # Obtaining total radiation + eradt_raw_data = eirene.eradt.sum(2) + sim.total_radiation = np.zeros((ni, nj)) + sim.total_radiation[1:-1, 1:-1] = eradt_raw_data + + sim.eirene_simulation = eirene + + return sim diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 12e8ccf..d7cf6f8 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -63,8 +63,6 @@ def __init__(self, mesh, species_list): self._ion_temperature = None self._neutral_temperature = None self._species_density = None - self._radial_particle_flux = None - self._radial_area = None self._velocities = None self._velocities_cartesian = None self._total_rad = None @@ -161,34 +159,6 @@ def species_density(self, value): self._species_density = value - @property - def radial_particle_flux(self): - """ - Blah - :return: - """ - return self._radial_particle_flux - - @radial_particle_flux.setter - def radial_particle_flux(self, value): - _check_array("radial_particle_flux", value, (self.mesh.nx, self.mesh.ny - 1, len(self.species_list))) - - self._radial_particle_flux = value - - @property - def radial_area(self): - """ - Blah - :return: - """ - return self._radial_area - - @radial_area.setter - def radial_area(self, value): - _check_array("radial_area", value, (self.mesh.nx, self.mesh.ny - 1)) - - self._radial_area = value - @property def velocities(self): return self._velocities_vectors @@ -339,8 +309,6 @@ def __getstate__(self): 'electron_density': self._electron_density, 'species_list': self._species_list, 'species_density': self._species_density, - 'radial_particle_flux': self._radial_particle_flux, - 'radial_area': self._radial_area, 'velocities': self._velocities, 'velocities_cartesian': self._velocities_cartesian, 'inside_mesh': self._inside_mesh, @@ -361,8 +329,6 @@ def __setstate__(self, state): self._electron_density = state['electron_density'] self._species_list = state['species_list'] self._species_density = state['species_density'] - self._radial_particle_flux = state['radial_particle_flux'] - self._radial_area = state['radial_area'] self._velocities = state['velocities'] self._velocities_cartesian = state['velocities_cartesian'] self._inside_mesh = state['inside_mesh'] From 9b0e9c11569472c25d17d884716c1e67db57d0e8 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Mon, 7 Sep 2020 14:46:38 +0300 Subject: [PATCH 08/25] Added exception handling to load_solps_from_mdsplus() for better support of B2.5 stand-alone simulations --- cherab/solps/formats/mdsplus.py | 55 ++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index a2f7fcc..d802ed5 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -37,7 +37,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): :rtype: SOLPSSimulation """ - from MDSplus import Connection as MDSConnection + from MDSplus import Connection as MDSConnection, mdsExceptions # Setup connection to server conn = MDSConnection(mds_server) @@ -78,22 +78,28 @@ def load_solps_from_mdsplus(mds_server, ref_number): sim.species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) # Load the neutral atom density from Eirene if available - dab2 = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() - if isinstance(dab2, np.ndarray): - # Replace the species densities - neutral_densities = np.swapaxes(dab2, 0, 2) - - neutral_i = 0 # counter for neutral atoms - for k, sp in enumerate(sim.species_list): - charge = sp[1] - if charge == 0: - sim.species_density[:, :, k] = neutral_densities[:, :, neutral_i] - neutral_i += 1 + try: + dab2 = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() + if isinstance(dab2, np.ndarray): + # Replace the species densities + neutral_densities = np.swapaxes(dab2, 0, 2) + + neutral_i = 0 # counter for neutral atoms + for k, sp in enumerate(sim.species_list): + charge = sp[1] + if charge == 0: + sim.species_density[:, :, k] = neutral_densities[:, :, neutral_i] + neutral_i += 1 + except mdsExceptions.TreeNNF: + pass # Load the neutral atom temperature from Eirene if available - tab2 = conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data() - if isinstance(tab2, np.ndarray): - sim.neutral_temperature = np.swapaxes(tab2, 0, 2) + try: + tab2 = conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data() + if isinstance(tab2, np.ndarray): + sim.neutral_temperature = np.swapaxes(tab2, 0, 2) + except mdsExceptions.TreeNNF: + pass # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. velocities = np.zeros((ni, nj, len(sim.species_list), 3)) @@ -129,14 +135,19 @@ def load_solps_from_mdsplus(mds_server, ref_number): linerad = np.sum(linerad, axis=2) brmrad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data(), 0, 2) brmrad = np.sum(brmrad, axis=2) - neurad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data() - if neurad is not None: # need to cope with fact that neurad may not be present!!! - if len(neurad.shape) == 3: - neurad = np.swapaxes(np.abs(np.sum(neurad, axis=0)), 0, 1) + + # need to cope with fact that neurad may not be present!!! + try: + neurad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data() + if isinstance(neurad, np.ndarray): + if len(neurad.shape) == 3: + neurad = np.swapaxes(np.abs(np.sum(neurad, axis=0)), 0, 1) + else: + neurad = np.swapaxes(np.abs(neurad), 0, 1) else: - neurad = np.swapaxes(np.abs(neurad), 0, 1) - else: - neurad = np.zeros(brmrad.shape) + neurad = 0 + except mdsExceptions.TreeNNF: + neurad = 0 sim.total_radiation = (linerad + brmrad + neurad) / mesh.vol From b840d3f4463264339e4c70a3a43889d98716e664 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Mon, 7 Sep 2020 20:33:11 +0300 Subject: [PATCH 09/25] Renamed parallel direction to poloidal in SOLPSMesh to match SOLPS notations. --- cherab/solps/mesh_geometry.py | 36 +++++++++++++++++------------------ cherab/solps/solps_plasma.py | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index dd67662..52e1d2a 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -66,13 +66,13 @@ def __init__(self, r, z, vol): self.vessel = None - # Calculating parallel basis vector - self._parallel_basis_vector = np.zeros((self._nx, self._ny, 2)) + # Calculating poloidal basis vector + self._poloidal_basis_vector = np.zeros((self._nx, self._ny, 2)) vec_r = r[:, :, 1] - r[:, :, 0] vec_z = z[:, :, 1] - z[:, :, 0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) - self._parallel_basis_vector[:, :, 0] = vec_r / vec_magn - self._parallel_basis_vector[:, :, 1] = vec_z / vec_magn + self._poloidal_basis_vector[:, :, 0] = vec_r / vec_magn + self._poloidal_basis_vector[:, :, 1] = vec_z / vec_magn # Calculating radial basis vector self._radial_basis_vector = np.zeros((self._nx, self._ny, 2)) @@ -83,14 +83,14 @@ def __init__(self, r, z, vol): self._radial_basis_vector[:, :, 1] = vec_z / vec_magn # For convertion from Cartesian to poloidal - self._inv_det = 1. / (self._parallel_basis_vector[:, :, 0] * self._radial_basis_vector[:, :, 1] - - self._parallel_basis_vector[:, :, 1] * self._radial_basis_vector[:, :, 0]) + self._inv_det = 1. / (self._poloidal_basis_vector[:, :, 0] * self._radial_basis_vector[:, :, 1] - + self._poloidal_basis_vector[:, :, 1] * self._radial_basis_vector[:, :, 0]) # Test for basis vector calculation # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._radial_basis_vector[:, 0, 0], self._radial_basis_vector[:, 0, 1], color='k') - # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._parallel_basis_vector[:, 0, 0], self._parallel_basis_vector[:, 0, 1], color='r') + # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._poloidal_basis_vector[:, 0, 0], self._poloidal_basis_vector[:, 0, 1], color='r') # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._radial_basis_vector[:, -1, 0], self._radial_basis_vector[:, -1, 1], color='k') - # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._parallel_basis_vector[:, -1, 0], self._parallel_basis_vector[:, -1, 1], color='r') + # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._poloidal_basis_vector[:, -1, 0], self._poloidal_basis_vector[:, -1, 1], color='r') # plt.gca().set_aspect('equal') # plt.show() @@ -187,11 +187,11 @@ def triangles(self): return self._triangles @property - def parallel_basis_vector(self): + def poloidal_basis_vector(self): """ - Array of 2D parallel basis vectors for grid cells. + Array of 2D poloidal basis vectors for grid cells. - For each cell there is a parallel and radial basis vector. + For each cell there is a poloidal and radial basis vector. Any vector on the poloidal grid can be converted to cartesian with the following transformation. bx = (p_x r_x) ( b_p ) @@ -199,14 +199,14 @@ def parallel_basis_vector(self): :return: ndarray with shape (nx, ny, 2). """ - return self._parallel_basis_vector + return self._poloidal_basis_vector @property def radial_basis_vector(self): """ Array of 2D radial basis vectors for grid cells. - For each cell there is a parallel and radial basis vector. + For each cell there is a poloidal and radial basis vector. Any vector on the poloidal grid can be converted to cartesian with the following transformation. bx = (p_x r_x) ( b_p ) @@ -248,13 +248,13 @@ def to_cartesian(self, vec_pol): """ Converts the 2D vector defined on mesh from poloidal to cartesian coordinates. :param ndarray vec_pol: Array of 2D vector with with shape (nx, ny, 2). - [:, :, 0] - parallel component, [:, :, 1] - radial component + [:, :, 0] - poloidal component, [:, :, 1] - radial component :return: ndarray with shape (nx, ny, 2) """ vec_cart = np.zeros((self._nx, self._ny, 2)) - vec_cart[:, :, 0] = self._parallel_basis_vector[:, :, 0] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 0] * vec_pol[:, :, 1] - vec_cart[:, :, 1] = self._parallel_basis_vector[:, :, 1] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 1] * vec_pol[:, :, 1] + vec_cart[:, :, 0] = self._poloidal_basis_vector[:, :, 0] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 0] * vec_pol[:, :, 1] + vec_cart[:, :, 1] = self._poloidal_basis_vector[:, :, 1] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 1] * vec_pol[:, :, 1] return vec_cart @@ -269,8 +269,8 @@ def to_poloidal(self, vec_cart): vec_pol = np.zeros((self._nx, self._ny, 2)) vec_pol[:, :, 0] = self._inv_det * (self._radial_basis_vector[:, :, 1] * vec_cart[:, :, 0] - self._radial_basis_vector[:, :, 0] * vec_cart[:, :, 1]) - vec_pol[:, :, 1] = self._inv_det * (self._parallel_basis_vector[:, :, 0] * vec_cart[:, :, 1] - - self._parallel_basis_vector[:, :, 1] * vec_cart[:, :, 0]) + vec_pol[:, :, 1] = self._inv_det * (self._poloidal_basis_vector[:, :, 0] * vec_cart[:, :, 1] - + self._poloidal_basis_vector[:, :, 1] * vec_cart[:, :, 0]) return vec_pol diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index d7cf6f8..82d0a36 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -242,7 +242,7 @@ def total_radiation_volume(self): @property def b_field(self): """ - Magnetic B field at each mesh cell in mesh cell coordinates (b_parallel, b_radial, b_toroidal). + Magnetic B field at each mesh cell in mesh cell coordinates (b_poloidal, b_radial, b_toroidal). """ if self._b_field_vectors is None: raise RuntimeError("Magnetic field not available for this simulation.") From 8a082e710a6c86594aac742b6e532839a9280d6c Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 9 Sep 2020 01:02:13 +0300 Subject: [PATCH 10/25] Added fluxes to velocity conversion to adress #36. --- cherab/solps/formats/balance.py | 63 ++++++-- cherab/solps/formats/mdsplus.py | 159 +++++++++++-------- cherab/solps/formats/raw_simulation_files.py | 106 ++++++++----- cherab/solps/mesh_geometry.py | 54 ++++++- cherab/solps/solps_plasma.py | 141 +++++++++++++++- 5 files changed, 397 insertions(+), 126 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 51cdd79..3a3d2da 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -41,16 +41,7 @@ def load_solps_from_balance(balance_filename): fhandle = netcdf.netcdf_file(balance_filename, 'r') # Load SOLPS mesh geometry - cr_x = fhandle.variables['crx'].data.copy() - cr_z = fhandle.variables['cry'].data.copy() - vol = fhandle.variables['vol'].data.copy() - - # Re-arrange the array dimensions in the way CHERAB expects... - cr_x = np.moveaxis(cr_x, 0, -1) - cr_z = np.moveaxis(cr_z, 0, -1) - - # Create the SOLPS mesh - mesh = SOLPSMesh(cr_x, cr_z, vol) + mesh = load_mesh_from_netcdf(fhandle) # Load each plasma species in simulation @@ -58,7 +49,7 @@ def load_solps_from_balance(balance_filename): n_species = len(fhandle.variables['am'].data) for i in range(n_species): - # Extract the nuclear charge + # Extract the nuclear charge if fhandle.variables['species'].data[i, 1] == b'D': zn = 1 if fhandle.variables['species'].data[i, 1] == b'C': @@ -75,7 +66,7 @@ def load_solps_from_balance(balance_filename): isotope = lookup_isotope(zn, number=am) species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same - # If we only need to populate species_list, there is probably a faster way to do this... + # If we only need to populate species_list, there is probably a faster way to do this... species_list.append((species, charge)) sim = SOLPSSimulation(mesh, species_list) @@ -114,24 +105,24 @@ def load_solps_from_balance(balance_filename): # Calculate the total radiated power if eirene_run: # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / vol + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / mesh.vol # Electron energy loss due to interactions with neutrals if 'eirene_mc_eael_she_bal' in fhandle.variables.keys(): - eirene_ecoolrate = np.sum(fhandle.variables['eirene_mc_eael_she_bal'].data, axis=0) / vol + eirene_ecoolrate = np.sum(fhandle.variables['eirene_mc_eael_she_bal'].data, axis=0) / mesh.vol # Ionisation rate from EIRENE, needed to calculate the energy loss to overcome the ionisation potential of atoms if 'eirene_mc_papl_sna_bal' in fhandle.variables.keys(): - eirene_potential_loss = rydberg_energy * np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1, :, :] * el_charge / vol + eirene_potential_loss = rydberg_energy * np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1, :, :] * el_charge / mesh.vol # This will be negative (energy sink); multiply by -1 sim.total_radiation = -1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss)) else: # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / vol + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / mesh.vol - potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0) / vol + potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0) / mesh.vol # Save total radiated power to the simulation object sim.total_radiation = rydberg_energy * el_charge * potential_loss - b2_ploss @@ -139,3 +130,41 @@ def load_solps_from_balance(balance_filename): fhandle.close() return sim + + +def load_mesh_from_netcdf(fhandle): + + # Load SOLPS mesh geometry + r = fhandle.variables['crx'].data.copy() + z = fhandle.variables['cry'].data.copy() + vol = fhandle.variables['vol'].data.copy() + + # Re-arrange the array dimensions in the way CHERAB expects... + r = np.moveaxis(r, 0, -1) + z = np.moveaxis(z, 0, -1) + + # Loading neighbouring cell indices + neighbix = np.zeros(r.shape, dtype=np.int) + neighbiy = np.zeros(r.shape, dtype=np.int) + + neighbix[:, :, 0] = fhandle.variables['leftix'].data.copy().astype(np.int) # poloidal prev. + neighbix[:, :, 1] = fhandle.variables['bottomix'].data.copy().astype(np.int) # radial prev. + neighbix[:, :, 2] = fhandle.variables['rightix'].data.copy().astype(np.int) # poloidal next + neighbix[:, :, 3] = fhandle.variables['topix'].data.copy().astype(np.int) # radial next + + neighbiy[:, :, 0] = fhandle.variables['leftiy'].data.copy().astype(np.int) + neighbiy[:, :, 1] = fhandle.variables['bottomiy'].data.copy().astype(np.int) + neighbiy[:, :, 2] = fhandle.variables['rightiy'].data.copy().astype(np.int) + neighbiy[:, :, 3] = fhandle.variables['topiy'].data.copy().astype(np.int) + + # In SOLPS cell indexing starts with -1 (guarding cell), but in SOLPSMesh -1 means no neighbour. + if neighbix.min() < -1 or neighbiy.min() < -1: + neighbix += 1 + neighbiy += 1 + neighbix[neighbix == r.shape[0]] = -1 + neighbiy[neighbiy == r.shape[1]] = -1 + + # Create the SOLPS mesh + mesh = SOLPSMesh(r, z, vol, neighbix, neighbiy) + + return mesh diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index d802ed5..ca0d0b8 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -22,7 +22,7 @@ from cherab.core.atomic.elements import lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, b2_flux_to_velocity, eirene_flux_to_velocity from matplotlib import pyplot as plt @@ -44,19 +44,22 @@ def load_solps_from_mdsplus(mds_server, ref_number): conn.openTree('solps', ref_number) # Load SOLPS mesh geometry and lookup arrays - mesh = load_mesh_from_mdsplus(conn) + mesh = load_mesh_from_mdsplus(conn, mdsExceptions) - # Load each plasma species + # Load each plasma species in simulation ns = conn.get('\SOLPS::TOP.IDENT.NS').data() # Number of species zn = conn.get('\SOLPS::TOP.SNAPSHOT.GRID.ZN').data().astype(np.int) # Nuclear charge am = np.round(conn.get('\SOLPS::TOP.SNAPSHOT.GRID.AM').data()).astype(np.int) # Atomic mass number charge = conn.get('\SOLPS::TOP.SNAPSHOT.GRID.ZA').data().astype(np.int) # Ionisation/charge species_list = [] + neutral_indx = [] for i in range(ns): isotope = lookup_isotope(zn[i], number=am[i]) species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same species_list.append((species, charge[i])) + if charge[i] == 0: + neutral_indx.append(i) sim = SOLPSSimulation(mesh, species_list) ni = mesh.nx @@ -75,55 +78,59 @@ def load_solps_from_mdsplus(mds_server, ref_number): sim.ion_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TI').data(), 0, 1) # Load species density - sim.species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) + species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) - # Load the neutral atom density from Eirene if available - try: - dab2 = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() - if isinstance(dab2, np.ndarray): - # Replace the species densities - neutral_densities = np.swapaxes(dab2, 0, 2) - - neutral_i = 0 # counter for neutral atoms - for k, sp in enumerate(sim.species_list): - charge = sp[1] - if charge == 0: - sim.species_density[:, :, k] = neutral_densities[:, :, neutral_i] - neutral_i += 1 - except mdsExceptions.TreeNNF: - pass - - # Load the neutral atom temperature from Eirene if available - try: - tab2 = conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data() - if isinstance(tab2, np.ndarray): - sim.neutral_temperature = np.swapaxes(tab2, 0, 2) - except mdsExceptions.TreeNNF: - pass + # Load parallel velocity + parallel_velocity = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) - # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. - velocities = np.zeros((ni, nj, len(sim.species_list), 3)) - velocities[:, :, :, 0] = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) + # Load poloidal and radial particle fluxes for velocity calculation + poloidal_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAX').data(), 0, 2) + radial_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) - ################################################ - # Calculate the species' velocity distribution # + # B2 fluxes are defined between cells, so correcting array shapes if needed + if poloidal_flux.shape[0] == ni - 1: + poloidal_flux = np.vstack((np.zeros((1, nj, ns)), poloidal_flux)) - # calculate field component ratios for velocity conversion - bplane2 = sim.b_field[:, :, 0]**2 + sim.b_field[:, :, 2]**2 - parallel_to_toroidal_ratio = sim.b_field[:, :, 0] * sim.b_field[:, :, 2] / bplane2 + if radial_flux.shape[1] == nj - 1: + radial_flux = np.hstack((np.zeros((ni, 1, ns)), radial_flux)) - # Calculate toroidal velocity components - velocities[:, :, :, 2] = velocities[:, :, :, 0] * parallel_to_toroidal_ratio[:, :, None] + # Obtaining velocity from B2 flux + velocities_cartesian = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cartesian) - # Radial velocity is obtained from radial particle flux - radial_particle_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) # radial particle flux - radial_area = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.SY').data(), 0, 1) # radial contact area - for k, sp in enumerate(sim.species_list): - i, j = np.where(sim.species_density[:, :-1, k] > 0) # radial_area array corresponds to [:, 1:] in mesh, so maybe [:, 1:, k] - velocities[i, j, k, 1] = radial_particle_flux[i, j, k] / radial_area[i, j] / sim.species_density[i, j, k] + # Obtaining additional data from EIRENE and replacing data for neutrals - sim.velocities = velocities - # sim.velocities_cartesian is created authomatically + b2_standalone = False + try: + # Replace the species densities + neutral_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data(), 0, 2) + species_density[:, :, neutral_indx] = neutral_density + except (mdsExceptions.TreeNNF, np.AxisError): + print("Warning! This is B2 stand-alone simulation.") + b2_standalone = True + + if not b2_standalone: + # Obtaining neutral atom velocity from EIRENE flux + # Note that if the output for fluxes was turned off, PFLA and RFLA' are all zeros + try: + neutral_poloidal_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.PFLA').data(), 0, 2) + neutral_radial_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RFLA').data(), 0, 2) + + if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): + neutral_velocities_cartesian = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, + parallel_velocity[:, :, neutral_indx], sim.b_field_cartesian) + + velocities_cartesian[:, :, neutral_indx, :] = neutral_velocities_cartesian + except (mdsExceptions.TreeNNF, np.AxisError): + pass + + # Obtaining neutral temperatures + try: + sim.neutral_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data(), 0, 2) + except (mdsExceptions.TreeNNF, np.AxisError): + pass + + sim.species_density = species_density + sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities ############################### # Load extra data from server # @@ -131,22 +138,25 @@ def load_solps_from_mdsplus(mds_server, ref_number): #################### # Integrated power # - linerad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQRAD').data(), 0, 2) - linerad = np.sum(linerad, axis=2) - brmrad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data(), 0, 2) - brmrad = np.sum(brmrad, axis=2) + try: + linerad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQRAD').data(), 0, 2) + linerad = np.sum(linerad, axis=2) + except (mdsExceptions.TreeNNF, np.AxisError): + linerad = 0 - # need to cope with fact that neurad may not be present!!! try: - neurad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data() - if isinstance(neurad, np.ndarray): - if len(neurad.shape) == 3: - neurad = np.swapaxes(np.abs(np.sum(neurad, axis=0)), 0, 1) - else: - neurad = np.swapaxes(np.abs(neurad), 0, 1) + brmrad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data(), 0, 2) + brmrad = np.sum(brmrad, axis=2) + except (mdsExceptions.TreeNNF, np.AxisError): + brmrad = 0 + + try: + eneutrad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data() + if np.ndim(eneutrad) == 3: # this will not return error if eneutrad is not np.ndarray + neurad = np.swapaxes(np.abs(np.sum(eneutrad, axis=0)), 0, 1) else: - neurad = 0 - except mdsExceptions.TreeNNF: + neurad = np.swapaxes(np.abs(eneutrad), 0, 1) + except (mdsExceptions.TreeNNF, np.AxisError): neurad = 0 sim.total_radiation = (linerad + brmrad + neurad) / mesh.vol @@ -154,30 +164,51 @@ def load_solps_from_mdsplus(mds_server, ref_number): return sim -def load_mesh_from_mdsplus(mds_connection): +def load_mesh_from_mdsplus(mds_connection, mdsExceptions): """ Load the SOLPS mesh geometry for a given MDSplus connection. :param mds_connection: MDSplus connection object. Already set to the SOLPS tree with pulse #ID. + :param mdsExceptions: MDSplus mdsExceptions module for error handling. """ # Load the R, Z coordinates of the cell vertices, original coordinates are (4, 38, 98) # re-arrange axes (4, 38, 98) => (98, 38, 4) - x = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:R').data(), 0, 2) + r = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:R').data(), 0, 2) z = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:Z').data(), 0, 2) vol = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.VOL').data(), 0, 1) + # Loading neighbouring cell indices + neighbix = np.zeros(r.shape, dtype=np.int) + neighbiy = np.zeros(r.shape, dtype=np.int) + + neighbix[:, :, 0] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIX').data().astype(np.int), 0, 1) + neighbix[:, :, 1] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIX').data().astype(np.int), 0, 1) + neighbix[:, :, 2] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIX').data().astype(np.int), 0, 1) + neighbix[:, :, 3] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIX').data().astype(np.int), 0, 1) + + neighbiy[:, :, 0] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIY').data().astype(np.int), 0, 1) + neighbiy[:, :, 1] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIY').data().astype(np.int), 0, 1) + neighbiy[:, :, 2] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIY').data().astype(np.int), 0, 1) + neighbiy[:, :, 3] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIY').data().astype(np.int), 0, 1) + + neighbix[neighbix == r.shape[0]] = -1 + neighbiy[neighbiy == r.shape[1]] = -1 + # build mesh object - mesh = SOLPSMesh(x, z, vol) + mesh = SOLPSMesh(r, z, vol, neighbix, neighbiy) ############################# # Add additional parameters # ############################# # add the vessel geometry - vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data() - if isinstance(vessel, np.ndarray): - mesh.vessel = vessel + try: + vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data() + if isinstance(vessel, np.ndarray): + mesh.vessel = vessel + except mdsExceptions.TreeNNF: + pass return mesh diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 58c4bcf..2d6033e 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -28,7 +28,7 @@ from cherab.solps.eirene import load_fort44_file from cherab.solps.b2.parse_b2_block_file import load_b2f_file from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, b2_flux_to_velocity, eirene_flux_to_velocity # Code based on script by Felix Reimold (2016) @@ -59,7 +59,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): RuntimeError("No B2 b2fstate file found in SOLPS output directory.") if not(os.path.isfile(eirene_fort44_file)): - print("Warning! No EIRENE fort.44 file found in SOLPS output directory. Assuming B2.5 stand-alone simulation.") + print("Warning! No EIRENE fort.44 file found in SOLPS output directory. Assuming B2 stand-alone simulation.") b2_standalone = True else: # Load data for neutral species from EIRENE output file @@ -69,7 +69,8 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Load SOLPS mesh geometry _, _, geom_data_dict = load_b2f_file(mesh_file_path, debug=debug) # geom_data_dict is needed also for magnetic field - mesh = SOLPSMesh(geom_data_dict['crx'], geom_data_dict['cry'], geom_data_dict['vol']) + mesh = create_mesh_from_geom_data(geom_data_dict) + ni = mesh.nx nj = mesh.ny @@ -77,6 +78,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Load each plasma species in simulation species_list = [] + neutral_indx = [] for i in range(len(sim_info_dict['zn'])): zn = int(sim_info_dict['zn'][i]) # Nuclear charge @@ -85,10 +87,12 @@ def load_solps_from_raw_output(simulation_path, debug=False): isotope = lookup_isotope(zn, number=am) species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same species_list.append((species, charge)) + if charge == 0: # updating neutral index + neutral_indx.append(i) sim = SOLPSSimulation(mesh, species_list) - # Load magnetic field + # Load magnetic field sim.b_field = geom_data_dict['bb'][:, :, :3] # sim.b_field_cartesian is created authomatically @@ -100,53 +104,48 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.ion_temperature = mesh_data_dict['ti'] / elementary_charge # Load species density - sim.species_density = mesh_data_dict['na'] + species_density = mesh_data_dict['na'] - if not b2_standalone: - # Replacing B2 neutral densities with EIRENE ones - da_raw_data = eirene.da - neutral_i = 0 # counter for neutral atoms - for k, sp in enumerate(sim.species_list): - charge = sp[1] - if charge == 0: - sim.species_density[1:-1, 1:-1, k] = da_raw_data[:, :, neutral_i] - neutral_i += 1 + # Load parallel velocity + parallel_velocity = mesh_data_dict['ua'] - # TODO: Eirene data (TOP.SNAPSHOT.PFLA, TOP.SNAPSHOT.RFLA) should be used for neutral atoms. - velocities = np.zeros((ni, nj, len(sim.species_list), 3)) - velocities[:, :, :, 0] = mesh_data_dict['ua'] + # Load poloidal and radial particle fluxes for velocity calculation + poloidal_flux = mesh_data_dict['fna'][:, :, ::2] + radial_flux = mesh_data_dict['fna'][:, :, 1::2] - ################################################ - # Calculate the species' velocity distribution # + # Obtaining velocity from B2 flux + velocities_cartesian = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cartesian) - # calculate field component ratios for velocity conversion - bplane2 = sim.b_field[:, :, 0]**2 + sim.b_field[:, :, 2]**2 - parallel_to_toroidal_ratio = sim.b_field[:, :, 0] * sim.b_field[:, :, 2] / bplane2 + if not b2_standalone: + # Obtaining additional data from EIRENE and replacing data for neutrals + # Note EIRENE data grid is slightly smaller than SOLPS grid, for example (98, 38) => (96, 36) + # Need to pad EIRENE data to fit inside larger B2 array - # Calculate toroidal velocity component - velocities[:, :, :, 2] = velocities[:, :, :, 0] * parallel_to_toroidal_ratio[:, :, None] + neutral_density = np.zeros((ni, nj, len(neutral_indx))) + neutral_density[1:-1, 1:-1, :] = eirene.da + species_density[:, :, neutral_indx] = neutral_density - # Radial velocity is obtained from radial particle flux - radial_particle_flux = mesh_data_dict['fna'][:, :, 1::2] + # Obtaining neutral atom velocity from EIRENE flux + # Note that if the output for fluxes was turned off, eirene.ppa and eirene.rpa are all zeros + if np.any(eirene.ppa) or np.any(eirene.rpa): + neutral_poloidal_flux = np.zeros((ni, nj, len(neutral_indx))) + neutral_poloidal_flux[1:-1, 1:-1, :] = eirene.ppa - vec_r = mesh.r[:, :, 1] - mesh.r[:, :, 0] - vec_z = mesh.z[:, :, 1] - mesh.z[:, :, 0] - radial_area = np.pi * (mesh.r[:, :, 1] + mesh.r[:, :, 0]) * np.sqrt(vec_r**2 + vec_z**2) + neutral_radial_flux = np.zeros((ni, nj, len(neutral_indx))) + neutral_radial_flux[1:-1, 1:-1, :] = eirene.rpa - for k, sp in enumerate(sim.species_list): - i, j = np.where(sim.species_density[:, :, k] > 0) - velocities[i, j, k, 1] = radial_particle_flux[i, j, k] / radial_area[i, j] / sim.species_density[i, j, k] + neutral_parallel_velocity = np.zeros((ni, nj, len(neutral_indx))) # must be zero outside EIRENE grid + neutral_parallel_velocity[1:-1, 1:-1, :] = parallel_velocity[1:-1, 1:-1, neutral_indx] - sim.velocities = velocities - # sim.velocities_cartesian is created authomatically + neutral_velocities_cartesian = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, + neutral_parallel_velocity, sim.b_field_cartesian) - if not b2_standalone: - # Note EIRENE data grid is slightly smaller than SOLPS grid, for example (98, 38) => (96, 36) - # Need to pad EIRENE data to fit inside larger B2 array + velocities_cartesian[:, :, neutral_indx, :] = neutral_velocities_cartesian # Obtaining neutral temperatures ta = np.zeros((ni, nj, eirene.ta.shape[2])) ta[1:-1, 1:-1, :] = eirene.ta + # extrapolating for i in (0, -1): ta[i, 1:-1, :] = eirene.ta[i, :, :] ta[1:-1, i, :] = eirene.ta[:, i, :] @@ -161,4 +160,37 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.eirene_simulation = eirene + sim.species_density = species_density + sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities + return sim + +def create_mesh_from_geom_data(geom_data): + + r = geom_data['crx'] + z = geom_data['cry'] + vol = geom_data['vol'] + + # Loading neighbouring cell indices + neighbix = np.zeros(r.shape, dtype=np.int) + neighbiy = np.zeros(r.shape, dtype=np.int) + + neighbix[:, :, 0] = geom_data['leftix'].astype(np.int) # poloidal prev. + neighbix[:, :, 1] = geom_data['bottomix'].astype(np.int) # radial prev. + neighbix[:, :, 2] = geom_data['rightix'].astype(np.int) # poloidal next + neighbix[:, :, 3] = geom_data['topix'].astype(np.int) # radial next + + neighbiy[:, :, 0] = geom_data['leftiy'].astype(np.int) + neighbiy[:, :, 1] = geom_data['bottomiy'].astype(np.int) + neighbiy[:, :, 2] = geom_data['rightiy'].astype(np.int) + neighbiy[:, :, 3] = geom_data['topiy'].astype(np.int) + + # In SOLPS cell indexing starts with -1 (guarding cell), but in SOLPSMesh -1 means no neighbour. + neighbix += 1 + neighbiy += 1 + neighbix[neighbix == r.shape[0]] = -1 + neighbiy[neighbiy == r.shape[1]] = -1 + + mesh = SOLPSMesh(r, z, vol, neighbix, neighbiy) + + return mesh diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index 52e1d2a..c9422de 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -43,9 +43,17 @@ class SOLPSMesh: :param ndarray r: Array of cell vertex r coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). :param ndarray z: Array of cell vertex z coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). :param ndarray vol: Array of cell volumes. Example shape is (98 x 32). + :param ndarray neighbix: Array of poloidal indeces of neighbouring cells in order: left, bottom, right, top, + must be 3 dimensional. Example shape is (98 x 32 x 4). + In SOLPS notation: left/right - poloidal prev./next, bottom/top - radial prev./next. + Cell indexing starts with 0 and -1 means no neighbour. + :param ndarray neighbix: Array of radial indeces of neighbouring cells in order: left, bottom, right, top, + must be 3 dimensional. Example shape is (98 x 32 x 4). """ - def __init__(self, r, z, vol): + # TODO Make neighbix and neighbix optional in the future, as they can be reconstructed with _tri_index_loopup + + def __init__(self, r, z, vol, neighbix, neighbiy): if r.shape != z.shape: raise ValueError('Shape of r array: %s mismatch the shape of z array: %s.' % (r.shape, z.shape)) @@ -53,6 +61,12 @@ def __init__(self, r, z, vol): if vol.shape != r.shape[:-1]: raise ValueError('Shape of vol array: %s mismatch the grid dimentions: %s.' % (vol.shape, r.shape[:-1])) + if neighbix.shape != r.shape: + raise ValueError('Shape of neighbix array must be %s, but it is %s.' % (r.shape, neighbix.shape)) + + if neighbiy.shape != r.shape: + raise ValueError('Shape of neighbix array must be %s, but it is %s.' % (r.shape, neighbiy.shape)) + self._cr = r.sum(2) / 4. self._cz = z.sum(2) / 4. @@ -64,6 +78,9 @@ def __init__(self, r, z, vol): self._vol = vol + self._neighbix = neighbix.astype(np.int) + self._neighbiy = neighbiy.astype(np.int) + self.vessel = None # Calculating poloidal basis vector @@ -71,18 +88,25 @@ def __init__(self, r, z, vol): vec_r = r[:, :, 1] - r[:, :, 0] vec_z = z[:, :, 1] - z[:, :, 0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) - self._poloidal_basis_vector[:, :, 0] = vec_r / vec_magn - self._poloidal_basis_vector[:, :, 1] = vec_z / vec_magn + self._poloidal_basis_vector[:, :, 0] = np.divide(vec_r, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) + self._poloidal_basis_vector[:, :, 1] = np.divide(vec_z, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) + + # Calculating radial contact areas + self._radial_area = np.pi * (r[:, :, 1] + r[:, :, 0]) * vec_magn[:, :] # Calculating radial basis vector self._radial_basis_vector = np.zeros((self._nx, self._ny, 2)) vec_r = r[:, :, 2] - r[:, :, 0] vec_z = z[:, :, 2] - z[:, :, 0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) - self._radial_basis_vector[:, :, 0] = vec_r / vec_magn - self._radial_basis_vector[:, :, 1] = vec_z / vec_magn + self._radial_basis_vector[:, :, 0] = np.divide(vec_r, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) + self._radial_basis_vector[:, :, 1] = np.divide(vec_z, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) + + # Calculating poloidal contact areas + self._poloidal_area = np.pi * (r[:, :, 2] + r[:, :, 0]) * vec_magn[:, :] # For convertion from Cartesian to poloidal + # TODO Make it work with trianle cells self._inv_det = 1. / (self._poloidal_basis_vector[:, :, 0] * self._radial_basis_vector[:, :, 1] - self._poloidal_basis_vector[:, :, 1] * self._radial_basis_vector[:, :, 0]) @@ -161,6 +185,26 @@ def vol(self): """Volume/area of each grid cell.""" return self._vol + @property + def neighbix(self): + """Poloidal indeces of neighbouring cells in order: left, bottom, right, top.""" + return self._neighbix + + @property + def neighbiy(self): + """Radial indeces of neighbouring cells in order: left, bottom, right, top.""" + return self._neighbiy + + @property + def radial_area(self): + """Radial contact area.""" + return self._radial_area + + @property + def poloidal_area(self): + """Poloidal contact area.""" + return self._poloidal_area + @property def vertex_coordinates(self): """RZ-coordinates of unique vertices.""" diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 82d0a36..70fe625 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -56,7 +56,7 @@ def __init__(self, mesh, species_list): if not len(species_list): raise ValueError('Argument "species_list" must contain at least one species.') - self._species_list = tuple(species_list) # adding species is not allowed + self._species_list = tuple(species_list) # adding additional species is not allowed self._electron_temperature = None self._electron_density = None @@ -167,7 +167,7 @@ def velocities(self): def velocities(self, value): _check_array("velocities", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) - # Converting to cartesian system + # Converting to Cartesian coordinates self._velocities_cartesian = np.zeros(value.shape) self._velocities_cartesian[:, :, :, 2] = value[:, :, :, 2] for k in range(value.shape[2]): @@ -183,7 +183,7 @@ def velocities_cartesian(self): def velocities_cartesian(self, value): _check_array("velocities_cartesian", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) - # Converting to poloidal system + # Converting to poloidal coordinates self._velocities = np.zeros(value.shape) self._velocities[:, :, :, 2] = value[:, :, :, 2] for k in range(value.shape[2]): @@ -588,3 +588,138 @@ def prefer_element(isotope): return isotope.element return isotope + + +def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cartesian): + """ + Calculates velocities of neutral particles using B2 particle fluxes defined at cell faces. + + :param SOLPSMesh mesh: SOLPS simulation mesh. + :param ndarray density: Density of atoms in m-3. Must be 3 dimensiona array of + shape (mesh.nx, mesh.ny, num_atoms). + :param ndarray poloidal_flux: Poloidal flux of atoms in s-1. Must be a 3 dimensional array of + shape (mesh.nx, mesh.ny, num_atoms). + :param ndarray radial_flux: Radial flux of atoms in s-1. Must be a 3 dimensional array of + shape (mesh.nx, mesh.ny, num_atoms). + :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional + array of shape (mesh.nx, mesh.ny, num_atoms). + Parallel velocity is a velocity projection on magnetic + field direction. + :param ndarray b_field_cartesian: Magnetic field in Cartesian (R, Z, phi) coordinates. + Must be a 3 dimensional array of shape (mesh.nx, mesh.ny, 3). + + :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of + shape (mesh.nx, mesh.ny, num_atoms, 3) + """ + + nx = mesh.nx + ny = mesh.ny + ns = density.shape[2] + + _check_array('density', poloidal_flux, (nx, ny, ns)) + _check_array('poloidal_flux', poloidal_flux, (nx, ny, ns)) + _check_array('radial_flux', radial_flux, (nx, ny, ns)) + _check_array('parallel_velocity', parallel_velocity, (nx, ny, ns)) + _check_array('b_field_cartesian', b_field_cartesian, (nx, ny, 3)) + + poloidal_area = mesh.poloidal_area[:, :, None] + radial_area = mesh.radial_area[:, :, None] + leftix = mesh.neighbix[:, :, 0] # poloidal prev. + leftiy = mesh.neighbiy[:, :, 0] + bottomix = mesh.neighbix[:, :, 1] # radial prev. + bottomiy = mesh.neighbiy[:, :, 1] + rightix = mesh.neighbix[:, :, 2] # poloidal next. + rightiy = mesh.neighbiy[:, :, 2] + topix = mesh.neighbix[:, :, 3] # radial next. + topiy = mesh.neighbiy[:, :, 3] + + # Converting s-1 --> m-2 s-1 + poloidal_flux = np.divide(poloidal_flux, poloidal_area, out=np.zeros_like(poloidal_flux), where=poloidal_area > 0) + radial_flux = np.divide(radial_flux, radial_area, out=np.zeros_like(radial_flux), where=radial_area > 0) + + # Obtaining left velocity + dens_neighb = density[leftix, leftiy, :] # density in the left neighbouring cell + has_neighbour = ((leftix > -1) * (leftiy > -1))[:, :, None] # check if has left neighbour + neg_flux = (poloidal_flux < 0) * (density > 0) # will use density in this cell if flux is negative + pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour # will use density in neighbouring cell if flux is positive + velocity_left = np.divide(poloidal_flux, density, out=np.zeros((nx, ny, ns)), where=neg_flux) + velocity_left = np.divide(poloidal_flux, dens_neighb, out=velocity_left, where=pos_flux) + velocity_left = velocity_left[:, :, :, None] * mesh.poloidal_basis_vector[:, :, None, :] # to vector in Cartesian + + # Obtaining bottom velocity + dens_neighb = density[bottomix, bottomiy, :] + has_neighbour = ((bottomix > -1) * (bottomiy > -1))[:, :, None] + neg_flux = (radial_flux < 0) * (density > 0) + pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour + velocity_bottom = np.divide(radial_flux, density, out=np.zeros((nx, ny, ns)), where=neg_flux) + velocity_bottom = np.divide(radial_flux, dens_neighb, out=velocity_bottom, where=pos_flux) + velocity_bottom = velocity_bottom[:, :, :, None] * mesh.radial_basis_vector[:, :, None, :] # to Cartesian + + # Obtaining right and top velocities + velocity_right = velocity_left[rightix, rightiy, :, :] + velocity_right[(rightix < 0) + (rightiy < 0)] = 0 + + velocity_top = velocity_bottom[topix, topiy, :, :] + velocity_top[(topix < 0) + (topiy < 0)] = 0 + + vcart = np.zeros((nx, ny, ns, 3)) # velocities in Cartesian coordinates + + # Projection of velocity on RZ-plane + vcart[:, :, :, :2] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) + + # Obtaining toroidal velocity + b = b_field_cartesian[:, :, None, :] + bmagn = np.sqrt((b * b).sum(3)) + vcart[:, :, :, 2] = (parallel_velocity * bmagn - vcart[:, :, :, 0] * b[:, :, :, 0] - vcart[:, :, :, 1] * b[:, :, :, 1]) / b[:, :, :, 2] + + return vcart + + +def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cartesian): + """ + Calculates velocities of neutral particles using Eirene particle fluxes defined at cell centre. + + :param SOLPSMesh mesh: SOLPS simulation mesh. + :param ndarray density: Density of atoms in m-3. Must be 3 dimensiona array of + shape (mesh.nx, mesh.ny, num_atoms). + :param ndarray poloidal_flux: Poloidal flux of atoms in m-2 s-1. Must be a 3 dimensional array of + shape (mesh.nx, mesh.ny, num_atoms). + :param ndarray radial_flux: Radial flux of atoms in m-2 s-1. Must be a 3 dimensional array of + shape (mesh.nx, mesh.ny, num_atoms). + :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional + array of shape (mesh.nx, mesh.ny, num_atoms). + Parallel velocity is a velocity projection on magnetic + field direction. + :param ndarray b_field_cartesian: Magnetic field in Cartesian (R, Z, phi) coordinates. + Must be a 3 dimensional array of shape (mesh.nx, mesh.ny, 3). + + :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of + shape (mesh.nx, mesh.ny, num_atoms, 3) + """ + + nx = mesh.nx + ny = mesh.ny + ns = density.shape[2] + + _check_array('density', poloidal_flux, (nx, ny, ns)) + _check_array('poloidal_flux', poloidal_flux, (nx, ny, ns)) + _check_array('radial_flux', radial_flux, (nx, ny, ns)) + _check_array('parallel_velocity', parallel_velocity, (nx, ny, ns)) + _check_array('b_field_cartesian', b_field_cartesian, (nx, ny, 3)) + + # Obtaining velocity + poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros((nx, ny, ns)), where=(density > 0)) + radial_velocity = np.divide(radial_flux, density, out=np.zeros((nx, ny, ns)), where=(density > 0)) + + vcart = np.zeros((nx, ny, ns, 3)) # velocities in Cartesian coordinates + + # Projection of velocity on RZ-plane + vcart[:, :, :, :2] = (poloidal_velocity[:, :, :, None] * mesh.poloidal_basis_vector[:, :, None, :] + + radial_velocity[:, :, :, None] * mesh.radial_basis_vector[:, :, None, :]) # to vector in Cartesian + + # Obtaining toroidal velocity + b = b_field_cartesian[:, :, None, :] + bmagn = np.sqrt((b * b).sum(3)) + vcart[:, :, :, 2] = (parallel_velocity * bmagn - (vcart[:, :, :, 0] * b[:, :, :, 0] + vcart[:, :, :, 1] * b[:, :, :, 1])) / b[:, :, :, 2] + + return vcart From df6ad34c60f2456a17a57a90d9b8a448a4586809 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 9 Sep 2020 01:34:46 +0300 Subject: [PATCH 11/25] Removed modification of assigned SOLPSSimulation attributes to simplify future implementation of #20 via setters. --- cherab/solps/formats/balance.py | 9 +++++---- cherab/solps/formats/raw_simulation_files.py | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 3a3d2da..96791ad 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -81,8 +81,7 @@ def load_solps_from_balance(balance_filename): sim.ion_temperature = fhandle.variables['ti'].data.copy() / el_charge tmp = fhandle.variables['na'].data.copy() - tmp = np.moveaxis(tmp, 0, -1) - sim.species_density = tmp + species_density = np.moveaxis(tmp, 0, -1) # Load the neutrals data try: @@ -94,14 +93,16 @@ def load_solps_from_balance(balance_filename): # the values calculated by EIRENE - do the same for other neutrals? if 'dab2' in fhandle.variables.keys(): if D0_indx is not None: - b2_len = np.shape(sim.species_density[:, :, D0_indx])[-1] + b2_len = np.shape(species_density[:, :, D0_indx])[-1] eirene_len = np.shape(fhandle.variables['dab2'].data)[-1] - sim.species_density[:, :, D0_indx] = fhandle.variables['dab2'].data[0, :, 0:b2_len - eirene_len] + species_density[:, :, D0_indx] = fhandle.variables['dab2'].data[0, :, 0:b2_len - eirene_len] eirene_run = True else: eirene_run = False + sim.species_density = species_density + # Calculate the total radiated power if eirene_run: # Total radiated power from B2, not including neutrals diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 2d6033e..a60834b 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -155,8 +155,9 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Obtaining total radiation eradt_raw_data = eirene.eradt.sum(2) - sim.total_radiation = np.zeros((ni, nj)) - sim.total_radiation[1:-1, 1:-1] = eradt_raw_data + total_radiation = np.zeros((ni, nj)) + total_radiation[1:-1, 1:-1] = eradt_raw_data + sim.total_radiation = total_radiation sim.eirene_simulation = eirene From 9ce89bbc0226cc873c7c796350ee3ffa76e469d8 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 9 Sep 2020 11:55:37 +0300 Subject: [PATCH 12/25] Added set_..() and get_..() interfaces to SOLPSSimulation to make easier future implementation of #20. --- cherab/solps/formats/balance.py | 12 +- cherab/solps/formats/mdsplus.py | 17 +-- cherab/solps/formats/raw_simulation_files.py | 16 +- cherab/solps/solps_plasma.py | 150 +++++++++++++------ 4 files changed, 126 insertions(+), 69 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 96791ad..4a491b7 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -74,11 +74,11 @@ def load_solps_from_balance(balance_filename): # TODO: add code to load SOLPS velocities and magnetic field from files # Load electron species - sim.electron_temperature = fhandle.variables['te'].data.copy() / el_charge - sim.electron_density = fhandle.variables['ne'].data.copy() + sim.set_electron_temperature(fhandle.variables['te'].data.copy() / el_charge) + sim.set_electron_density(fhandle.variables['ne'].data.copy()) # Load ion temperature - sim.ion_temperature = fhandle.variables['ti'].data.copy() / el_charge + sim.set_ion_temperature(fhandle.variables['ti'].data.copy() / el_charge) tmp = fhandle.variables['na'].data.copy() species_density = np.moveaxis(tmp, 0, -1) @@ -101,7 +101,7 @@ def load_solps_from_balance(balance_filename): else: eirene_run = False - sim.species_density = species_density + sim.set_species_density(species_density) # Calculate the total radiated power if eirene_run: @@ -117,7 +117,7 @@ def load_solps_from_balance(balance_filename): eirene_potential_loss = rydberg_energy * np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1, :, :] * el_charge / mesh.vol # This will be negative (energy sink); multiply by -1 - sim.total_radiation = -1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss)) + sim.set_total_radiation(-1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss))) else: # Total radiated power from B2, not including neutrals @@ -126,7 +126,7 @@ def load_solps_from_balance(balance_filename): potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0) / mesh.vol # Save total radiated power to the simulation object - sim.total_radiation = rydberg_energy * el_charge * potential_loss - b2_ploss + sim.set_total_radiation(rydberg_energy * el_charge * potential_loss - b2_ploss) fhandle.close() diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index ca0d0b8..7df876a 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -27,7 +27,6 @@ from matplotlib import pyplot as plt -# TODO: violates interface of SOLPSSimulation.... puts numpy arrays in the object where they should be function2D def load_solps_from_mdsplus(mds_server, ref_number): """ Load a SOLPS simulation from a MDSplus server. @@ -67,15 +66,15 @@ def load_solps_from_mdsplus(mds_server, ref_number): ########################## # Magnetic field vectors # - sim.b_field = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2)[:, :, :3] + sim.set_b_field(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2)[:, :, :3]) # sim.b_field_cartesian is created authomatically # Load electron temperature and density - sim.electron_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1) # (32, 98) => (98, 32) - sim.electron_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NE').data(), 0, 1) # (32, 98) => (98, 32) + sim.set_electron_temperature(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1)) # (32, 98) => (98, 32) + sim.set_electron_density(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NE').data(), 0, 1)) # (32, 98) => (98, 32) # Load ion temperature - sim.ion_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TI').data(), 0, 1) + sim.set_ion_temperature(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TI').data(), 0, 1)) # Load species density species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) @@ -125,12 +124,12 @@ def load_solps_from_mdsplus(mds_server, ref_number): # Obtaining neutral temperatures try: - sim.neutral_temperature = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data(), 0, 2) + sim.set_neutral_temperature(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data(), 0, 2)) except (mdsExceptions.TreeNNF, np.AxisError): pass - sim.species_density = species_density - sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities + sim.set_species_density(species_density) + sim.set_velocities_cartesian(velocities_cartesian) # this also updates sim.velocities ############################### # Load extra data from server # @@ -159,7 +158,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): except (mdsExceptions.TreeNNF, np.AxisError): neurad = 0 - sim.total_radiation = (linerad + brmrad + neurad) / mesh.vol + sim.set_total_radiation((linerad + brmrad + neurad) / mesh.vol) return sim diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index a60834b..b29fc26 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -93,15 +93,15 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim = SOLPSSimulation(mesh, species_list) # Load magnetic field - sim.b_field = geom_data_dict['bb'][:, :, :3] + sim.set_b_field(geom_data_dict['bb'][:, :, :3]) # sim.b_field_cartesian is created authomatically # Load electron species - sim.electron_temperature = mesh_data_dict['te'] / elementary_charge - sim.electron_density = mesh_data_dict['ne'] + sim.set_electron_temperature(mesh_data_dict['te'] / elementary_charge) + sim.set_electron_density(mesh_data_dict['ne']) # Load ion temperature - sim.ion_temperature = mesh_data_dict['ti'] / elementary_charge + sim.set_ion_temperature(mesh_data_dict['ti'] / elementary_charge) # Load species density species_density = mesh_data_dict['na'] @@ -151,18 +151,18 @@ def load_solps_from_raw_output(simulation_path, debug=False): ta[1:-1, i, :] = eirene.ta[:, i, :] for i, j in ((0, 0), (0, -1), (-1, 0), (-1, -1)): ta[i, j, :] = eirene.ta[i, j, :] - sim.neutral_temperature = ta / elementary_charge + sim.set_neutral_temperature(ta / elementary_charge) # Obtaining total radiation eradt_raw_data = eirene.eradt.sum(2) total_radiation = np.zeros((ni, nj)) total_radiation[1:-1, 1:-1] = eradt_raw_data - sim.total_radiation = total_radiation + sim.set_total_radiation(total_radiation) sim.eirene_simulation = eirene - sim.species_density = species_density - sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities + sim.set_species_density(species_density) + sim.set_velocities_cartesian(velocities_cartesian) # this also updates sim.velocities return sim diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 70fe625..c3302cc 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -38,7 +38,20 @@ from .mesh_geometry import SOLPSMesh -# TODO: this interface is half broken - some routines expect internal data as arrays, others as function 3d +# TODO: This interface is half broken - some routines expect internal data as arrays, others as function 3d. +# +# In the future SOLPSSimulation should keep the data arrays in self._data dict, e.g. self._data["electron_temperature"], etc. +# Method self.set_electon_temperature(value) should set self._data["electron_temperature"] to value and +# initialise self._electron_temperature as a Discrete2DMesh (or AxisymmetricMapper, or SOLPSFunction3D), etc. +# Method self.get_electon_temperature() should return np.copy(self._data["electron_temperature"]), while +# self.electron_temperature should return a Discrete2DMesh (or AxisymmetricMapper, or SOLPSFunction3D) instance, etc. +# +# As an option: self.electron_temperature returns a Discrete2DMesh instance, self.electron_temperature_3d returns +# AxisymmetricMapper or SOLPSFunction3D (?). +# +# Also, there is some confusion in coordinates system names. We have curvilinear poloidal coordinates (poloidal, radial, toroidal), +# plane toroidal coordinates (R, Z, toroidal) and Cartesian coordinates (x, y, z). For now toroidal coordinates refered as Cartesian. + class SOLPSSimulation: def __init__(self, mesh, species_list): @@ -96,8 +109,11 @@ def electron_temperature(self): """ return self._electron_temperature - @electron_temperature.setter - def electron_temperature(self, value): + def get_electron_temperature(self): + + return self._electron_temperature + + def set_electron_temperature(self, value): _check_array("electron_temperature", value, (self.mesh.nx, self.mesh.ny)) self._electron_temperature = value @@ -110,8 +126,11 @@ def ion_temperature(self): """ return self._ion_temperature - @ion_temperature.setter - def ion_temperature(self, value): + def get_ion_temperature(self): + + return self._ion_temperature + + def set_ion_temperature(self, value): _check_array("ion_temperature", value, (self.mesh.nx, self.mesh.ny)) self._ion_temperature = value @@ -124,8 +143,11 @@ def neutral_temperature(self): """ return self._neutral_temperature - @neutral_temperature.setter - def neutral_temperature(self, value): + def get_neutral_temperature(self): + + return self._neutral_temperature + + def set_neutral_temperature(self, value): num_neutrals = len([sp for sp in self.species_list if sp[1] == 0]) _check_array("neutral_temperature", value, (self.mesh.nx, self.mesh.ny, num_neutrals)) @@ -139,8 +161,11 @@ def electron_density(self): """ return self._electron_density - @electron_density.setter - def electron_density(self, value): + def get_electron_density(self): + + return self._electron_density + + def set_electron_density(self, value): _check_array("electron_density", value, (self.mesh.nx, self.mesh.ny)) self._electron_density = value @@ -153,42 +178,62 @@ def species_density(self): """ return self._species_density - @species_density.setter - def species_density(self, value): + def get_species_density(self): + + return self._species_density + + def set_species_density(self, value): _check_array("species_density", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) self._species_density = value @property def velocities(self): - return self._velocities_vectors + """ + Velocities in poloidal coordinates (v_poloidal, v_radial, v_toroidal) for each species densities at each mesh cell. + :return: + """ + return self._velocities + + def get_velocities(self): + + return self._velocities - @velocities.setter - def velocities(self, value): + def set_velocities(self, value): _check_array("velocities", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) # Converting to Cartesian coordinates - self._velocities_cartesian = np.zeros(value.shape) - self._velocities_cartesian[:, :, :, 2] = value[:, :, :, 2] + velocities_cartesian = np.zeros(value.shape) + velocities_cartesian[:, :, :, 2] = value[:, :, :, 2] for k in range(value.shape[2]): - self._velocities_cartesian[:, :, k, :2] = self.mesh.to_cartesian(value[:, :, k, :2]) + velocities_cartesian[:, :, k, :2] = self.mesh.to_cartesian(value[:, :, k, :2]) + self._velocities_cartesian = velocities_cartesian self._velocities = value @property def velocities_cartesian(self): + """ + Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. + :return: + """ + # TODO: If converted to SOLPSVectorFunction3D, should porbably return at (vx, vy, vz) at (x, y, z). (?) + return self._velocities_cartesian + + def get_velocities_cartesian(self): + return self._velocities_cartesian - @velocities_cartesian.setter - def velocities_cartesian(self, value): + def set_velocities_cartesian(self, value): _check_array("velocities_cartesian", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) # Converting to poloidal coordinates - self._velocities = np.zeros(value.shape) - self._velocities[:, :, :, 2] = value[:, :, :, 2] + velocities = np.zeros(value.shape) + velocities[:, :, :, 2] = value[:, :, :, 2] for k in range(value.shape[2]): - self._velocities[:, :, k, :2] = self.mesh.to_poloidal(value[:, :, k, :2]) + velocities[:, :, k, :2] = self.mesh.to_poloidal(value[:, :, k, :2]) + self._velocities = value self._velocities_cartesian = value @property @@ -215,8 +260,11 @@ def total_radiation(self): else: return self._total_rad - @total_radiation.setter - def total_radiation(self, value): + def get_total_radiation(self): + + return self._total_rad + + def set_total_radiation(self, value): _check_array("total_radiation", value, (self.mesh.nx, self.mesh.ny)) self._total_rad = value @@ -249,36 +297,45 @@ def b_field(self): else: return self._b_field_vectors - @b_field.setter - def b_field(self, value): + def get_b_field(self): + + return self._b_field_vectors + + def set_b_field(self, value): _check_array("b_field", value, (self.mesh.nx, self.mesh.ny, 3)) # Converting to cartesian system - self._b_field_vectors_cartesian = np.zeros(value.shape) - self._b_field_vectors_cartesian[:, :, 2] = value[:, :, 2] - self._b_field_vectors_cartesian[:, :, :2] = self.mesh.to_cartesian(value[:, :, :2]) + b_field_vectors_cartesian = np.zeros(value.shape) + b_field_vectors_cartesian[:, :, 2] = value[:, :, 2] + b_field_vectors_cartesian[:, :, :2] = self.mesh.to_cartesian(value[:, :, :2]) + self._b_field_vectors_cartesian = b_field_vectors_cartesian self._b_field_vectors = value @property def b_field_cartesian(self): """ - Magnetic B field at each mesh cell in cartesian coordinates (Bx, By, Bz). + Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). """ + # TODO: If converted to SOLPSVectorFunction3D, should porbably return at (Bx, By, Bz) at (x, y, z). (?) if self._b_field_vectors_cartesian is None: raise RuntimeError("Magnetic field not available for this simulation.") else: return self._b_field_vectors_cartesian - @b_field_cartesian.setter - def b_field_cartesian(self, value): + def get_b_field_cartesian(self): + + return self._b_field_vectors_cartesian + + def set_b_field_cartesian(self, value): _check_array("b_field_cartesian", value, (self.mesh.nx, self.mesh.ny, 3)) # Converting to poloidal system - self._b_field_vectors = np.zeros(value.shape) - self._b_field_vectors[:, :, 2] = value[:, :, 2] - self._b_field_vectors[:, :, :2] = self.mesh.to_poloidal(value[:, :, :2]) + b_field_vectors = np.zeros(value.shape) + b_field_vectors[:, :, 2] = value[:, :, 2] + b_field_vectors[:, :, :2] = self.mesh.to_poloidal(value[:, :, :2]) + self._b_field_vectors = b_field_vectors self._b_field_vectors_cartesian = value @property @@ -502,27 +559,28 @@ def create_plasma(self, parent=None, transform=None, name=None): tri_to_grid = self.mesh.triangle_to_grid_map try: - plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.b_field_cartesian) + plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.get_b_field_cartesian()) + # self.get_.. always returns data even if self.b_field_cartesian will return function in the future except RuntimeError: print('Warning! No magnetic field data available for this simulation.') # Create electron species - triangle_data = _map_data_onto_triangles(self.electron_temperature) + triangle_data = _map_data_onto_triangles(self.get_electron_temperature()) electron_te_interp = Discrete2DMesh(mesh.vertex_coordinates, mesh.triangles, triangle_data, limit=False) electron_temp = AxisymmetricMapper(electron_te_interp) - triangle_data = _map_data_onto_triangles(self.electron_density) + triangle_data = _map_data_onto_triangles(self.get_electron_density()) electron_dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) plasma.electron_distribution = Maxwellian(electron_dens, electron_temp, electron_velocity, electron_mass) # Ion temperature - triangle_data = _map_data_onto_triangles(self.ion_temperature) + triangle_data = _map_data_onto_triangles(self.get_ion_temperature()) ion_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - if self.velocities_cartesian is None: + if self.get_velocities_cartesian() is None: print('Warning! No velocity field data available for this simulation.') - if self.neutral_temperature is None: + if self.get_neutral_temperature ()is None: print('Warning! No neutral atom temperature data available for this simulation.') neutral_i = 0 # neutrals count @@ -531,22 +589,22 @@ def create_plasma(self, parent=None, transform=None, name=None): species_type = sp[0] charge = sp[1] - triangle_data = _map_data_onto_triangles(self.species_density[:, :, k]) + triangle_data = _map_data_onto_triangles(self.get_species_density()[:, :, k]) dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, self.species_density[:, :, k]) # Create the velocity vector lookup function - if self.velocities_cartesian is not None: - velocity = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.velocities_cartesian[:, :, k, :]) + if self.get_velocities_cartesian() is not None: + velocity = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.get_velocities_cartesian()[:, :, k, :]) else: velocity = lambda x, y, z: Vector3D(0, 0, 0) - if charge or self.neutral_temperature is None: # ions or neutral atoms (neutral temperature is not available) + if charge or self.get_neutral_temperature() is None: # ions or neutral atoms (neutral temperature is not available) distribution = Maxwellian(dens, ion_temp, velocity, species_type.atomic_weight * atomic_mass) else: # neutral atoms with neutral temperature - triangle_data = _map_data_onto_triangles(self.neutral_temperature[:, :, neutral_i]) + triangle_data = _map_data_onto_triangles(self.get_neutral_temperature()[:, :, neutral_i]) neutral_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) distribution = Maxwellian(dens, neutral_temp, velocity, species_type.atomic_weight * atomic_mass) neutral_i += 1 From 52a516231ad5b5fadde885d8bedab36346ad0f3a Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 9 Sep 2020 13:15:54 +0300 Subject: [PATCH 13/25] Made assign_fort44_parser() less strict to adress #32 --- cherab/solps/eirene/parser/fort44.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/cherab/solps/eirene/parser/fort44.py b/cherab/solps/eirene/parser/fort44.py index 230e4ce..78a8056 100644 --- a/cherab/solps/eirene/parser/fort44.py +++ b/cherab/solps/eirene/parser/fort44.py @@ -48,7 +48,7 @@ def load_fort44_file(file_path, debug=False): # Look up file parsing function and call it to obtain for44 block and update data dictionary parser = assign_fort44_parser(data["version"]) - return parser(file_path) + return parser(file_path, debug) def assign_fort44_parser(file_version): @@ -57,13 +57,17 @@ def assign_fort44_parser(file_version): :param file_version: Fort44 file version from the file header. :return: Parsing function object """ + fort44_supported_versios = ( + 20081111, + 20130210, + 20170328, + ) - fort44_parser_library = { - 20170328: load_fort44_2017, - 20130210: load_fort44_2013 - } + if file_version not in fort44_supported_versios: + print("Warning! Version {} of fort.44 file has not been tested with this parser.".format(file_version)) - if file_version in fort44_parser_library.keys(): - return fort44_parser_library[file_version] - else: - raise ValueError("Can't read version {} fort.44 file".format(file_version)) + if file_version >= 20170328: + + return load_fort44_2017 + + return load_fort44_2013 From 48445d9f716652d08635d968983b8eae2020a128 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 9 Sep 2020 16:10:45 +0300 Subject: [PATCH 14/25] Fixed errors with pickle save/load. --- cherab/solps/formats/balance.py | 6 +++--- cherab/solps/formats/mdsplus.py | 2 +- cherab/solps/formats/raw_pickle.py | 2 +- cherab/solps/formats/raw_simulation_files.py | 2 +- cherab/solps/mesh_geometry.py | 4 +++- cherab/solps/solps_plasma.py | 15 ++++++++++----- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 4a491b7..0648436 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -23,7 +23,7 @@ from raysect.core.math.function.float import Discrete2DMesh from cherab.core.math.mappers import AxisymmetricMapper -from cherab.core.atomic.elements import lookup_isotope, deuterium +from cherab.core.atomic.elements import lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element @@ -67,7 +67,7 @@ def load_solps_from_balance(balance_filename): species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same # If we only need to populate species_list, there is probably a faster way to do this... - species_list.append((species, charge)) + species_list.append((species.name, charge)) sim = SOLPSSimulation(mesh, species_list) @@ -85,7 +85,7 @@ def load_solps_from_balance(balance_filename): # Load the neutrals data try: - D0_indx = sim.species_list.index((deuterium, 0)) + D0_indx = sim.species_list.index(("deuterium", 0)) except ValueError: D0_indx = None diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index 7df876a..d45baaa 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -56,7 +56,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): for i in range(ns): isotope = lookup_isotope(zn[i], number=am[i]) species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same - species_list.append((species, charge[i])) + species_list.append((species.name, charge[i])) if charge[i] == 0: neutral_indx.append(i) diff --git a/cherab/solps/formats/raw_pickle.py b/cherab/solps/formats/raw_pickle.py index 6b33fa2..c1e0e86 100644 --- a/cherab/solps/formats/raw_pickle.py +++ b/cherab/solps/formats/raw_pickle.py @@ -26,7 +26,7 @@ def load_solps_from_pickle(filename): file_handle = open(filename, 'rb') state = pickle.load(file_handle) - mesh = SOLPSMesh(state['mesh']['cr_r'], state['mesh']['cr_z'], state['mesh']['vol']) + mesh = SOLPSMesh(state['mesh']['r'], state['mesh']['z'], state['mesh']['vol'], state['mesh']['neighbix'], state['mesh']['neighbiy']) simulation = SOLPSSimulation(mesh, state['species_list']) simulation.__setstate__(state) file_handle.close() diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index b29fc26..245bd01 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -86,7 +86,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): charge = int(sim_info_dict['zamax'][i]) # Ionisation/charge isotope = lookup_isotope(zn, number=am) species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same - species_list.append((species, charge)) + species_list.append((species.name, charge)) if charge == 0: # updating neutral index neutral_indx.append(i) diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index c9422de..55c7477 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -284,7 +284,9 @@ def __getstate__(self): state = { 'r': self._r, 'z': self._z, - 'vol': self._vol + 'vol': self._vol, + 'neighbix': self._neighbix, + 'neighbiy': self._neighbiy } return state diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index c3302cc..8eebc15 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -31,6 +31,7 @@ # CHERAB core imports from cherab.core import Plasma, Species, Maxwellian from cherab.core.math.mappers import AxisymmetricMapper +from cherab.core.atomic.elements import lookup_isotope, lookup_element # This SOLPS package imports from cherab.solps.eirene import Eirene @@ -96,7 +97,7 @@ def mesh(self): @property def species_list(self): """ - Tuple of species elements in the form (Element/Isotope, charge). + Tuple of species elements in the form (species name, charge). :return: """ return self._species_list @@ -359,7 +360,7 @@ def eirene_simulation(self, value): def __getstate__(self): state = { - 'mesh': self.mesh.__getstate__(), + 'mesh': self._mesh.__getstate__(), 'electron_temperature': self._electron_temperature, 'ion_temperature': self._ion_temperature, 'neutral_temperature': self._neutral_temperature, @@ -379,7 +380,7 @@ def __getstate__(self): return state def __setstate__(self, state): - self.mesh = SOLPSMesh(**state['mesh']) + self._mesh = SOLPSMesh(**state['mesh']) self._electron_temperature = state['electron_temperature'] self._ion_temperature = state['ion_temperature'] self._neutral_temperature = state['neutral_temperature'] @@ -580,13 +581,17 @@ def create_plasma(self, parent=None, transform=None, name=None): if self.get_velocities_cartesian() is None: print('Warning! No velocity field data available for this simulation.') - if self.get_neutral_temperature ()is None: + if self.get_neutral_temperature() is None: print('Warning! No neutral atom temperature data available for this simulation.') neutral_i = 0 # neutrals count for k, sp in enumerate(self.species_list): - species_type = sp[0] + try: + species_type = lookup_element(sp[0]) + except ValueError: + species_type = lookup_isotope(sp[0]) + charge = sp[1] triangle_data = _map_data_onto_triangles(self.get_species_density()[:, :, k]) From 1e551440ecdf33e69b0e8e1f1652ffc070f1de6b Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Thu, 10 Sep 2020 14:50:06 +0300 Subject: [PATCH 15/25] Corrected the typo --- cherab/solps/eirene/parser/fort44.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cherab/solps/eirene/parser/fort44.py b/cherab/solps/eirene/parser/fort44.py index 78a8056..06ba344 100644 --- a/cherab/solps/eirene/parser/fort44.py +++ b/cherab/solps/eirene/parser/fort44.py @@ -57,13 +57,13 @@ def assign_fort44_parser(file_version): :param file_version: Fort44 file version from the file header. :return: Parsing function object """ - fort44_supported_versios = ( + fort44_supported_versions = ( 20081111, 20130210, 20170328, ) - if file_version not in fort44_supported_versios: + if file_version not in fort44_supported_versions: print("Warning! Version {} of fort.44 file has not been tested with this parser.".format(file_version)) if file_version >= 20170328: From 1884289ac42b023499107e536f5868ab5bb798ea Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Thu, 10 Sep 2020 22:32:00 +0300 Subject: [PATCH 16/25] Made all array row-major and inverted indexing. Removed get_*(), set_*() methods, added setters instead. --- cherab/solps/b2/parse_b2_block_file.py | 8 +- cherab/solps/eirene/eirene.py | 12 +- cherab/solps/eirene/parser/utility.py | 8 +- cherab/solps/formats/balance.py | 60 ++-- cherab/solps/formats/mdsplus.py | 107 +++--- cherab/solps/formats/raw_simulation_files.py | 76 ++--- cherab/solps/mesh_geometry.py | 115 ++++--- cherab/solps/solps_3d_functions.pyx | 4 +- cherab/solps/solps_plasma.py | 324 ++++++++----------- 9 files changed, 332 insertions(+), 382 deletions(-) diff --git a/cherab/solps/b2/parse_b2_block_file.py b/cherab/solps/b2/parse_b2_block_file.py index 03a51c5..ccda206 100755 --- a/cherab/solps/b2/parse_b2_block_file.py +++ b/cherab/solps/b2/parse_b2_block_file.py @@ -47,16 +47,16 @@ def _make_solps_data_object(_data): # Multiple 2D data field (e.g. na) if number > nxyg: - _data = np.array(_data).reshape((nxg, nyg, int(number / nxyg)), order='F') + _data = np.array(_data).reshape((int(number / nxyg), nyg, nxg)) if debug: - print('Mesh data field {} with dimensions: {:d} x {:d} x {:d}'.format(name, nxg, nyg, int(number/nxyg))) + print('Mesh data field {} with dimensions: {:d} x {:d} x {:d}'.format(name, int(number / nxyg), nyg, nxg)) return MESH_DATA, _data # 2D data field (e.g. ne) elif number == nxyg: - _data = np.array(_data).reshape((nxg, nyg), order='F') + _data = np.array(_data).reshape((nyg, nxg)) if debug: - print('Mesh data field {} with dimensions: {:d} x {:d}'.format(name, nxg, nyg)) + print('Mesh data field {} with dimensions: {:d} x {:d}'.format(name, nyg, nxg)) return MESH_DATA, _data # Additional information field (e.g. zamin) diff --git a/cherab/solps/eirene/eirene.py b/cherab/solps/eirene/eirene.py index 7f8fcc2..425bf5c 100755 --- a/cherab/solps/eirene/eirene.py +++ b/cherab/solps/eirene/eirene.py @@ -91,7 +91,7 @@ def __init__(self, nx, ny, na, nm, ni, ns, species_labels, version=None, @property def nx(self): """ - Number of grid cells in the x direction + Number of grid cells in the poloidal direction :rtype: int """ @@ -100,7 +100,7 @@ def nx(self): @property def ny(self): """ - Number of grid cells in the y direction + Number of grid cells in the radial direction :rtype: int """ @@ -426,13 +426,13 @@ def eradt(self, value): self._check_dimensions(value, 1) self._eradt = value - def _check_dimensions(self, data, dim2): + def _check_dimensions(self, data, dim0): """ Checks compatibility of the data array dimension with the species number and grid size. - :param dim2: size of the 2nd dimenion + :param dim0: size of the 1st dimenion :return: """ - if not data.shape == (self._nx, self._ny, dim2): + if not data.shape == (dim0, self._ny, self._nx): raise ValueError("Array with shape {0} obtained, but {1} expected".format(data.shape, - (self._nx, self._ny, dim2))) + (dim0, self._ny, self._nx))) diff --git a/cherab/solps/eirene/parser/utility.py b/cherab/solps/eirene/parser/utility.py index 72e842d..f3d0944 100644 --- a/cherab/solps/eirene/parser/utility.py +++ b/cherab/solps/eirene/parser/utility.py @@ -25,9 +25,9 @@ def read_block44(file_handle, ns, nx, ny): :param file_handle: A python core file handle object as a result of a call to open('./fort.44'). :param int ns: total number of species - :param int nx: number of grid x cells - :param int ny: number of grid y cells - :return: ndarray of data with shape [nx, ny, ns] + :param int nx: number of grid poloidal cells + :param int ny: number of grid radial cells + :return: ndarray of data with shape [ns, ny, nx] """ data = [] npoints = ns * nx * ny @@ -37,5 +37,5 @@ def read_block44(file_handle, ns, nx, ny): # This is a comment line. Ignore continue data.extend(line) - data = np.asarray(data, dtype=float).reshape((nx, ny, ns), order='F') + data = np.asarray(data, dtype=float).reshape((ns, ny, nx)) return data diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index 0648436..a77a714 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -74,14 +74,13 @@ def load_solps_from_balance(balance_filename): # TODO: add code to load SOLPS velocities and magnetic field from files # Load electron species - sim.set_electron_temperature(fhandle.variables['te'].data.copy() / el_charge) - sim.set_electron_density(fhandle.variables['ne'].data.copy()) + sim.electron_temperature = fhandle.variables['te'].data.copy().T / el_charge + sim.electron_density = fhandle.variables['ne'].data.copy().T # Load ion temperature - sim.set_ion_temperature(fhandle.variables['ti'].data.copy() / el_charge) + sim.ion_temperature = fhandle.variables['ti'].data.copy().T / el_charge - tmp = fhandle.variables['na'].data.copy() - species_density = np.moveaxis(tmp, 0, -1) + species_density = np.transpose(fhandle.variables['na'].data, (0, 2, 1)) # Load the neutrals data try: @@ -93,40 +92,42 @@ def load_solps_from_balance(balance_filename): # the values calculated by EIRENE - do the same for other neutrals? if 'dab2' in fhandle.variables.keys(): if D0_indx is not None: - b2_len = np.shape(species_density[:, :, D0_indx])[-1] - eirene_len = np.shape(fhandle.variables['dab2'].data)[-1] - species_density[:, :, D0_indx] = fhandle.variables['dab2'].data[0, :, 0:b2_len - eirene_len] + b2_len = np.shape(species_density[D0_indx])[-2] + dab2 = fhandle.variables['dab2'].data.copy()[0].T + eirene_len = np.shape(fhandle.variables['dab2'].data)[-2] + species_density[D0_indx] = dab2[0:b2_len - eirene_len, :] eirene_run = True else: eirene_run = False - sim.set_species_density(species_density) + sim.species_density = species_density # Calculate the total radiated power if eirene_run: # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / mesh.vol + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0).T / mesh.vol # Electron energy loss due to interactions with neutrals if 'eirene_mc_eael_she_bal' in fhandle.variables.keys(): - eirene_ecoolrate = np.sum(fhandle.variables['eirene_mc_eael_she_bal'].data, axis=0) / mesh.vol + eirene_ecoolrate = np.sum(fhandle.variables['eirene_mc_eael_she_bal'].data, axis=0).T / mesh.vol # Ionisation rate from EIRENE, needed to calculate the energy loss to overcome the ionisation potential of atoms if 'eirene_mc_papl_sna_bal' in fhandle.variables.keys(): - eirene_potential_loss = rydberg_energy * np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1, :, :] * el_charge / mesh.vol + tmp = np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1].T + eirene_potential_loss = rydberg_energy * tmp * el_charge / mesh.vol # This will be negative (energy sink); multiply by -1 - sim.set_total_radiation(-1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss))) + sim.total_radiation = -1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss)) else: # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / mesh.vol + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0).T / mesh.vol - potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0) / mesh.vol + potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0).T / mesh.vol # Save total radiated power to the simulation object - sim.set_total_radiation(rydberg_energy * el_charge * potential_loss - b2_ploss) + sim.total_radiation = rydberg_energy * el_charge * potential_loss - b2_ploss fhandle.close() @@ -136,33 +137,30 @@ def load_solps_from_balance(balance_filename): def load_mesh_from_netcdf(fhandle): # Load SOLPS mesh geometry - r = fhandle.variables['crx'].data.copy() - z = fhandle.variables['cry'].data.copy() - vol = fhandle.variables['vol'].data.copy() - # Re-arrange the array dimensions in the way CHERAB expects... - r = np.moveaxis(r, 0, -1) - z = np.moveaxis(z, 0, -1) + r = np.transpose(fhandle.variables['crx'].data, (0, 2, 1)) + z = np.transpose(fhandle.variables['cry'].data, (0, 2, 1)) + vol = fhandle.variables['vol'].data.copy().T # Loading neighbouring cell indices neighbix = np.zeros(r.shape, dtype=np.int) neighbiy = np.zeros(r.shape, dtype=np.int) - neighbix[:, :, 0] = fhandle.variables['leftix'].data.copy().astype(np.int) # poloidal prev. - neighbix[:, :, 1] = fhandle.variables['bottomix'].data.copy().astype(np.int) # radial prev. - neighbix[:, :, 2] = fhandle.variables['rightix'].data.copy().astype(np.int) # poloidal next - neighbix[:, :, 3] = fhandle.variables['topix'].data.copy().astype(np.int) # radial next + neighbix[0] = fhandle.variables['leftix'].data.copy().astype(np.int).T # poloidal prev. + neighbix[1] = fhandle.variables['bottomix'].data.copy().astype(np.int).T # radial prev. + neighbix[2] = fhandle.variables['rightix'].data.copy().astype(np.int).T # poloidal next + neighbix[3] = fhandle.variables['topix'].data.copy().astype(np.int).T # radial next - neighbiy[:, :, 0] = fhandle.variables['leftiy'].data.copy().astype(np.int) - neighbiy[:, :, 1] = fhandle.variables['bottomiy'].data.copy().astype(np.int) - neighbiy[:, :, 2] = fhandle.variables['rightiy'].data.copy().astype(np.int) - neighbiy[:, :, 3] = fhandle.variables['topiy'].data.copy().astype(np.int) + neighbiy[0] = fhandle.variables['leftiy'].data.copy().astype(np.int).T + neighbiy[1] = fhandle.variables['bottomiy'].data.copy().astype(np.int).T + neighbiy[2] = fhandle.variables['rightiy'].data.copy().astype(np.int).T + neighbiy[3] = fhandle.variables['topiy'].data.copy().astype(np.int).T # In SOLPS cell indexing starts with -1 (guarding cell), but in SOLPSMesh -1 means no neighbour. if neighbix.min() < -1 or neighbiy.min() < -1: neighbix += 1 neighbiy += 1 - neighbix[neighbix == r.shape[0]] = -1 + neighbix[neighbix == r.shape[2]] = -1 neighbiy[neighbiy == r.shape[1]] = -1 # Create the SOLPS mesh diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index d45baaa..8a62e18 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -61,37 +61,37 @@ def load_solps_from_mdsplus(mds_server, ref_number): neutral_indx.append(i) sim = SOLPSSimulation(mesh, species_list) - ni = mesh.nx - nj = mesh.ny + nx = mesh.nx + ny = mesh.ny ########################## # Magnetic field vectors # - sim.set_b_field(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.B').data(), 0, 2)[:, :, :3]) + sim.b_field = conn.get('\SOLPS::TOP.SNAPSHOT.B').data()[:3] # sim.b_field_cartesian is created authomatically # Load electron temperature and density - sim.set_electron_temperature(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TE').data(), 0, 1)) # (32, 98) => (98, 32) - sim.set_electron_density(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NE').data(), 0, 1)) # (32, 98) => (98, 32) + sim.electron_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TE').data() + sim.electron_density = conn.get('\SOLPS::TOP.SNAPSHOT.NE').data() # Load ion temperature - sim.set_ion_temperature(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TI').data(), 0, 1)) + sim.ion_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TI').data() # Load species density - species_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.NA').data(), 0, 2) + species_density = conn.get('\SOLPS::TOP.SNAPSHOT.NA').data() # Load parallel velocity - parallel_velocity = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.UA').data(), 0, 2) + parallel_velocity = conn.get('\SOLPS::TOP.SNAPSHOT.UA').data() # Load poloidal and radial particle fluxes for velocity calculation - poloidal_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAX').data(), 0, 2) - radial_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data(), 0, 2) + poloidal_flux = conn.get('\SOLPS::TOP.SNAPSHOT.FNAX').data() + radial_flux = conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data() # B2 fluxes are defined between cells, so correcting array shapes if needed - if poloidal_flux.shape[0] == ni - 1: - poloidal_flux = np.vstack((np.zeros((1, nj, ns)), poloidal_flux)) + if poloidal_flux.shape[2] == nx - 1: + poloidal_flux = np.concatenate((np.zeros((ns, ny, 1)), poloidal_flux), axis=2) - if radial_flux.shape[1] == nj - 1: - radial_flux = np.hstack((np.zeros((ni, 1, ns)), radial_flux)) + if radial_flux.shape[1] == ny - 1: + radial_flux = np.concatenate((np.zeros((ns, 1, nx)), radial_flux), axis=1) # Obtaining velocity from B2 flux velocities_cartesian = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cartesian) @@ -101,9 +101,10 @@ def load_solps_from_mdsplus(mds_server, ref_number): b2_standalone = False try: # Replace the species densities - neutral_density = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data(), 0, 2) - species_density[:, :, neutral_indx] = neutral_density - except (mdsExceptions.TreeNNF, np.AxisError): + neutral_density = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() + species_density[neutral_indx] = neutral_density[:] # this will throw a TypeError is neutral_density is not an array + + except (mdsExceptions.TreeNNF, TypeError): print("Warning! This is B2 stand-alone simulation.") b2_standalone = True @@ -111,25 +112,25 @@ def load_solps_from_mdsplus(mds_server, ref_number): # Obtaining neutral atom velocity from EIRENE flux # Note that if the output for fluxes was turned off, PFLA and RFLA' are all zeros try: - neutral_poloidal_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.PFLA').data(), 0, 2) - neutral_radial_flux = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RFLA').data(), 0, 2) + neutral_poloidal_flux = conn.get('\SOLPS::TOP.SNAPSHOT.PFLA').data()[:] + neutral_radial_flux = conn.get('\SOLPS::TOP.SNAPSHOT.RFLA').data()[:] if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): neutral_velocities_cartesian = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, - parallel_velocity[:, :, neutral_indx], sim.b_field_cartesian) + parallel_velocity[neutral_indx], sim.b_field_cartesian) - velocities_cartesian[:, :, neutral_indx, :] = neutral_velocities_cartesian - except (mdsExceptions.TreeNNF, np.AxisError): + velocities_cartesian[neutral_indx] = neutral_velocities_cartesian + except (mdsExceptions.TreeNNF, TypeError): pass # Obtaining neutral temperatures try: - sim.set_neutral_temperature(np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data(), 0, 2)) - except (mdsExceptions.TreeNNF, np.AxisError): + sim.neutral_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data()[:] + except (mdsExceptions.TreeNNF, TypeError): pass - sim.set_species_density(species_density) - sim.set_velocities_cartesian(velocities_cartesian) # this also updates sim.velocities + sim.species_density = species_density + sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities ############################### # Load extra data from server # @@ -138,27 +139,25 @@ def load_solps_from_mdsplus(mds_server, ref_number): #################### # Integrated power # try: - linerad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQRAD').data(), 0, 2) - linerad = np.sum(linerad, axis=2) - except (mdsExceptions.TreeNNF, np.AxisError): + linerad = np.sum(conn.get('\SOLPS::TOP.SNAPSHOT.RQRAD').data()[:], axis=0) + except (mdsExceptions.TreeNNF, TypeError): linerad = 0 try: - brmrad = np.swapaxes(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data(), 0, 2) - brmrad = np.sum(brmrad, axis=2) - except (mdsExceptions.TreeNNF, np.AxisError): + brmrad = np.sum(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data()[:], axis=0) + except (mdsExceptions.TreeNNF, TypeError): brmrad = 0 try: - eneutrad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data() - if np.ndim(eneutrad) == 3: # this will not return error if eneutrad is not np.ndarray - neurad = np.swapaxes(np.abs(np.sum(eneutrad, axis=0)), 0, 1) + eneutrad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data()[:] + if np.ndim(eneutrad) == 3: + neurad = np.abs(np.sum(eneutrad, axis=0)) else: - neurad = np.swapaxes(np.abs(eneutrad), 0, 1) - except (mdsExceptions.TreeNNF, np.AxisError): + neurad = np.abs(eneutrad) + except (mdsExceptions.TreeNNF, TypeError): neurad = 0 - sim.set_total_radiation((linerad + brmrad + neurad) / mesh.vol) + sim.total_radiation = (linerad + brmrad + neurad) / mesh.vol return sim @@ -172,27 +171,26 @@ def load_mesh_from_mdsplus(mds_connection, mdsExceptions): """ # Load the R, Z coordinates of the cell vertices, original coordinates are (4, 38, 98) - # re-arrange axes (4, 38, 98) => (98, 38, 4) - r = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:R').data(), 0, 2) - z = np.swapaxes(mds_connection.get('\TOP.SNAPSHOT.GRID:Z').data(), 0, 2) + r = mds_connection.get('\TOP.SNAPSHOT.GRID:R').data() + z = mds_connection.get('\TOP.SNAPSHOT.GRID:Z').data() - vol = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.VOL').data(), 0, 1) + vol = mds_connection.get('\SOLPS::TOP.SNAPSHOT.VOL').data() # Loading neighbouring cell indices neighbix = np.zeros(r.shape, dtype=np.int) neighbiy = np.zeros(r.shape, dtype=np.int) - neighbix[:, :, 0] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIX').data().astype(np.int), 0, 1) - neighbix[:, :, 1] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIX').data().astype(np.int), 0, 1) - neighbix[:, :, 2] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIX').data().astype(np.int), 0, 1) - neighbix[:, :, 3] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIX').data().astype(np.int), 0, 1) + neighbix[0] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIX').data().astype(np.int) + neighbix[1] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIX').data().astype(np.int) + neighbix[2] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIX').data().astype(np.int) + neighbix[3] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIX').data().astype(np.int) - neighbiy[:, :, 0] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIY').data().astype(np.int), 0, 1) - neighbiy[:, :, 1] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIY').data().astype(np.int), 0, 1) - neighbiy[:, :, 2] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIY').data().astype(np.int), 0, 1) - neighbiy[:, :, 3] = np.swapaxes(mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIY').data().astype(np.int), 0, 1) + neighbiy[0] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIY').data().astype(np.int) + neighbiy[1] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIY').data().astype(np.int) + neighbiy[2] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIY').data().astype(np.int) + neighbiy[3] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIY').data().astype(np.int) - neighbix[neighbix == r.shape[0]] = -1 + neighbix[neighbix == r.shape[2]] = -1 neighbiy[neighbiy == r.shape[1]] = -1 # build mesh object @@ -204,10 +202,9 @@ def load_mesh_from_mdsplus(mds_connection, mdsExceptions): # add the vessel geometry try: - vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data() - if isinstance(vessel, np.ndarray): - mesh.vessel = vessel - except mdsExceptions.TreeNNF: + vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data()[:] + mesh.vessel = vessel + except (mdsExceptions.TreeNNF, TypeError): pass return mesh diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 245bd01..487d578 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -71,8 +71,8 @@ def load_solps_from_raw_output(simulation_path, debug=False): mesh = create_mesh_from_geom_data(geom_data_dict) - ni = mesh.nx - nj = mesh.ny + ny = mesh.ny # radial + nx = mesh.nx # poloidal header_dict, sim_info_dict, mesh_data_dict = load_b2f_file(b2_state_file, debug=debug) @@ -93,15 +93,15 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim = SOLPSSimulation(mesh, species_list) # Load magnetic field - sim.set_b_field(geom_data_dict['bb'][:, :, :3]) + sim.b_field = geom_data_dict['bb'][:3] # sim.b_field_cartesian is created authomatically # Load electron species - sim.set_electron_temperature(mesh_data_dict['te'] / elementary_charge) - sim.set_electron_density(mesh_data_dict['ne']) + sim.electron_temperature = mesh_data_dict['te'] / elementary_charge + sim.electron_density = mesh_data_dict['ne'] # Load ion temperature - sim.set_ion_temperature(mesh_data_dict['ti'] / elementary_charge) + sim.ion_temperature = mesh_data_dict['ti'] / elementary_charge # Load species density species_density = mesh_data_dict['na'] @@ -110,8 +110,8 @@ def load_solps_from_raw_output(simulation_path, debug=False): parallel_velocity = mesh_data_dict['ua'] # Load poloidal and radial particle fluxes for velocity calculation - poloidal_flux = mesh_data_dict['fna'][:, :, ::2] - radial_flux = mesh_data_dict['fna'][:, :, 1::2] + poloidal_flux = mesh_data_dict['fna'][::2] + radial_flux = mesh_data_dict['fna'][1::2] # Obtaining velocity from B2 flux velocities_cartesian = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cartesian) @@ -121,48 +121,48 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Note EIRENE data grid is slightly smaller than SOLPS grid, for example (98, 38) => (96, 36) # Need to pad EIRENE data to fit inside larger B2 array - neutral_density = np.zeros((ni, nj, len(neutral_indx))) - neutral_density[1:-1, 1:-1, :] = eirene.da - species_density[:, :, neutral_indx] = neutral_density + neutral_density = np.zeros((len(neutral_indx), ny, nx)) + neutral_density[:, 1:-1, 1:-1] = eirene.da + species_density[neutral_indx] = neutral_density # Obtaining neutral atom velocity from EIRENE flux # Note that if the output for fluxes was turned off, eirene.ppa and eirene.rpa are all zeros if np.any(eirene.ppa) or np.any(eirene.rpa): - neutral_poloidal_flux = np.zeros((ni, nj, len(neutral_indx))) - neutral_poloidal_flux[1:-1, 1:-1, :] = eirene.ppa + neutral_poloidal_flux = np.zeros((len(neutral_indx), ny, nx)) + neutral_poloidal_flux[:, 1:-1, 1:-1] = eirene.ppa - neutral_radial_flux = np.zeros((ni, nj, len(neutral_indx))) - neutral_radial_flux[1:-1, 1:-1, :] = eirene.rpa + neutral_radial_flux = np.zeros((len(neutral_indx), ny, nx)) + neutral_radial_flux[:, 1:-1, 1:-1] = eirene.rpa - neutral_parallel_velocity = np.zeros((ni, nj, len(neutral_indx))) # must be zero outside EIRENE grid - neutral_parallel_velocity[1:-1, 1:-1, :] = parallel_velocity[1:-1, 1:-1, neutral_indx] + neutral_parallel_velocity = np.zeros((len(neutral_indx), ny, nx)) # must be zero outside EIRENE grid + neutral_parallel_velocity[:, 1:-1, 1:-1] = parallel_velocity[neutral_indx, 1:-1, 1:-1] neutral_velocities_cartesian = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, neutral_parallel_velocity, sim.b_field_cartesian) - velocities_cartesian[:, :, neutral_indx, :] = neutral_velocities_cartesian + velocities_cartesian[neutral_indx] = neutral_velocities_cartesian # Obtaining neutral temperatures - ta = np.zeros((ni, nj, eirene.ta.shape[2])) - ta[1:-1, 1:-1, :] = eirene.ta + ta = np.zeros((eirene.ta.shape[0], ny, nx)) + ta[:, 1:-1, 1:-1] = eirene.ta # extrapolating for i in (0, -1): - ta[i, 1:-1, :] = eirene.ta[i, :, :] - ta[1:-1, i, :] = eirene.ta[:, i, :] + ta[:, i, 1:-1] = eirene.ta[:, i, :] + ta[:, 1:-1, i] = eirene.ta[:, :, i] for i, j in ((0, 0), (0, -1), (-1, 0), (-1, -1)): - ta[i, j, :] = eirene.ta[i, j, :] - sim.set_neutral_temperature(ta / elementary_charge) + ta[:, i, j] = eirene.ta[:, i, j] + sim.neutral_temperature = ta / elementary_charge # Obtaining total radiation - eradt_raw_data = eirene.eradt.sum(2) - total_radiation = np.zeros((ni, nj)) + eradt_raw_data = eirene.eradt.sum(0) + total_radiation = np.zeros((ny, nx)) total_radiation[1:-1, 1:-1] = eradt_raw_data - sim.set_total_radiation(total_radiation) + sim.total_radiation = total_radiation sim.eirene_simulation = eirene - sim.set_species_density(species_density) - sim.set_velocities_cartesian(velocities_cartesian) # this also updates sim.velocities + sim.species_density = species_density + sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities return sim @@ -176,20 +176,20 @@ def create_mesh_from_geom_data(geom_data): neighbix = np.zeros(r.shape, dtype=np.int) neighbiy = np.zeros(r.shape, dtype=np.int) - neighbix[:, :, 0] = geom_data['leftix'].astype(np.int) # poloidal prev. - neighbix[:, :, 1] = geom_data['bottomix'].astype(np.int) # radial prev. - neighbix[:, :, 2] = geom_data['rightix'].astype(np.int) # poloidal next - neighbix[:, :, 3] = geom_data['topix'].astype(np.int) # radial next + neighbix[0] = geom_data['leftix'].astype(np.int) # poloidal prev. + neighbix[1] = geom_data['bottomix'].astype(np.int) # radial prev. + neighbix[2] = geom_data['rightix'].astype(np.int) # poloidal next + neighbix[3] = geom_data['topix'].astype(np.int) # radial next - neighbiy[:, :, 0] = geom_data['leftiy'].astype(np.int) - neighbiy[:, :, 1] = geom_data['bottomiy'].astype(np.int) - neighbiy[:, :, 2] = geom_data['rightiy'].astype(np.int) - neighbiy[:, :, 3] = geom_data['topiy'].astype(np.int) + neighbiy[0] = geom_data['leftiy'].astype(np.int) + neighbiy[1] = geom_data['bottomiy'].astype(np.int) + neighbiy[2] = geom_data['rightiy'].astype(np.int) + neighbiy[3] = geom_data['topiy'].astype(np.int) # In SOLPS cell indexing starts with -1 (guarding cell), but in SOLPSMesh -1 means no neighbour. neighbix += 1 neighbiy += 1 - neighbix[neighbix == r.shape[0]] = -1 + neighbix[neighbix == r.shape[2]] = -1 neighbiy[neighbiy == r.shape[1]] = -1 mesh = SOLPSMesh(r, z, vol, neighbix, neighbiy) diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index 55c7477..e261a51 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -40,15 +40,15 @@ class SOLPSMesh: triangle vertices. Therefore, each SOLPS rectangular cell is split into two triangular cells. The data points are later interpolated onto the vertex points. - :param ndarray r: Array of cell vertex r coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). - :param ndarray z: Array of cell vertex z coordinates, must be 3 dimensional. Example shape is (98 x 32 x 4). - :param ndarray vol: Array of cell volumes. Example shape is (98 x 32). + :param ndarray r: Array of cell vertex r coordinates, must be 3 dimensional. Example shape is (4 x 32 x 98). + :param ndarray z: Array of cell vertex z coordinates, must be 3 dimensional. Example shape is (4 x 32 x 98). + :param ndarray vol: Array of cell volumes. Example shape is (32 x 98). :param ndarray neighbix: Array of poloidal indeces of neighbouring cells in order: left, bottom, right, top, - must be 3 dimensional. Example shape is (98 x 32 x 4). + must be 3 dimensional. Example shape is (4 x 32 x 98). In SOLPS notation: left/right - poloidal prev./next, bottom/top - radial prev./next. Cell indexing starts with 0 and -1 means no neighbour. :param ndarray neighbix: Array of radial indeces of neighbouring cells in order: left, bottom, right, top, - must be 3 dimensional. Example shape is (98 x 32 x 4). + must be 3 dimensional. Example shape is (4 x 32 x 98). """ # TODO Make neighbix and neighbix optional in the future, as they can be reconstructed with _tri_index_loopup @@ -58,8 +58,8 @@ def __init__(self, r, z, vol, neighbix, neighbiy): if r.shape != z.shape: raise ValueError('Shape of r array: %s mismatch the shape of z array: %s.' % (r.shape, z.shape)) - if vol.shape != r.shape[:-1]: - raise ValueError('Shape of vol array: %s mismatch the grid dimentions: %s.' % (vol.shape, r.shape[:-1])) + if vol.shape != r.shape[1:]: + raise ValueError('Shape of vol array: %s mismatch the grid dimentions: %s.' % (vol.shape, r.shape[1:])) if neighbix.shape != r.shape: raise ValueError('Shape of neighbix array must be %s, but it is %s.' % (r.shape, neighbix.shape)) @@ -67,11 +67,11 @@ def __init__(self, r, z, vol, neighbix, neighbiy): if neighbiy.shape != r.shape: raise ValueError('Shape of neighbix array must be %s, but it is %s.' % (r.shape, neighbiy.shape)) - self._cr = r.sum(2) / 4. - self._cz = z.sum(2) / 4. + self._cr = r.sum(0) / 4. + self._cz = z.sum(0) / 4. - self._nx = r.shape[0] - self._ny = r.shape[1] + self._nx = r.shape[2] # poloidal + self._ny = r.shape[1] # radial self._r = r self._z = z @@ -84,37 +84,37 @@ def __init__(self, r, z, vol, neighbix, neighbiy): self.vessel = None # Calculating poloidal basis vector - self._poloidal_basis_vector = np.zeros((self._nx, self._ny, 2)) - vec_r = r[:, :, 1] - r[:, :, 0] - vec_z = z[:, :, 1] - z[:, :, 0] + self._poloidal_basis_vector = np.zeros((2, self._ny, self._nx)) + vec_r = r[1] - r[0] + vec_z = z[1] - z[0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) - self._poloidal_basis_vector[:, :, 0] = np.divide(vec_r, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) - self._poloidal_basis_vector[:, :, 1] = np.divide(vec_z, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) + self._poloidal_basis_vector[0] = np.divide(vec_r, vec_magn, out=np.zeros_like(vec_magn), where=(vec_magn > 0)) + self._poloidal_basis_vector[1] = np.divide(vec_z, vec_magn, out=np.zeros_like(vec_magn), where=(vec_magn > 0)) # Calculating radial contact areas - self._radial_area = np.pi * (r[:, :, 1] + r[:, :, 0]) * vec_magn[:, :] + self._radial_area = np.pi * (r[1] + r[0]) * vec_magn # Calculating radial basis vector - self._radial_basis_vector = np.zeros((self._nx, self._ny, 2)) - vec_r = r[:, :, 2] - r[:, :, 0] - vec_z = z[:, :, 2] - z[:, :, 0] + self._radial_basis_vector = np.zeros((2, self._ny, self._nx)) + vec_r = r[2] - r[0] + vec_z = z[2] - z[0] vec_magn = np.sqrt(vec_r**2 + vec_z**2) - self._radial_basis_vector[:, :, 0] = np.divide(vec_r, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) - self._radial_basis_vector[:, :, 1] = np.divide(vec_z, vec_magn, out=np.zeros((self._nx, self._ny)), where=(vec_magn > 0)) + self._radial_basis_vector[0] = np.divide(vec_r, vec_magn, out=np.zeros_like(vec_magn), where=(vec_magn > 0)) + self._radial_basis_vector[1] = np.divide(vec_z, vec_magn, out=np.zeros_like(vec_magn), where=(vec_magn > 0)) # Calculating poloidal contact areas - self._poloidal_area = np.pi * (r[:, :, 2] + r[:, :, 0]) * vec_magn[:, :] + self._poloidal_area = np.pi * (r[2] + r[0]) * vec_magn # For convertion from Cartesian to poloidal # TODO Make it work with trianle cells - self._inv_det = 1. / (self._poloidal_basis_vector[:, :, 0] * self._radial_basis_vector[:, :, 1] - - self._poloidal_basis_vector[:, :, 1] * self._radial_basis_vector[:, :, 0]) + self._inv_det = 1. / (self._poloidal_basis_vector[0] * self._radial_basis_vector[1] - + self._poloidal_basis_vector[1] * self._radial_basis_vector[0]) # Test for basis vector calculation - # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._radial_basis_vector[:, 0, 0], self._radial_basis_vector[:, 0, 1], color='k') - # plt.quiver(self._cr[:, 0], self._cz[:, 0], self._poloidal_basis_vector[:, 0, 0], self._poloidal_basis_vector[:, 0, 1], color='r') - # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._radial_basis_vector[:, -1, 0], self._radial_basis_vector[:, -1, 1], color='k') - # plt.quiver(self._cr[:, -1], self._cz[:, -1], self._poloidal_basis_vector[:, -1, 0], self._poloidal_basis_vector[:, -1, 1], color='r') + # plt.quiver(self._cr[0], self._cz[0], self._radial_basis_vector[0, 0], self._radial_basis_vector[1, 0], color='k') + # plt.quiver(self._cr[0], self._cz[0], self._poloidal_basis_vector[0, 0], self._poloidal_basis_vector[1, 0], color='r') + # plt.quiver(self._cr[-1], self._cz[-1], self._radial_basis_vector[0, -1], self._radial_basis_vector[1, -1], color='k') + # plt.quiver(self._cr[-1], self._cz[-1], self._poloidal_basis_vector[0, -1], self._poloidal_basis_vector[1, -1], color='r') # plt.gca().set_aspect('equal') # plt.show() @@ -133,31 +133,32 @@ def __init__(self, r, z, vol, neighbix, neighbiy): # Pull out the index number for each unique vertex in this rectangular cell. # Unusual vertex indexing is based on SOLPS output, see Matlab code extract from David Moulton. - self._triangles[0::2, 0] = unique_vertices[0::4] - self._triangles[0::2, 1] = unique_vertices[2::4] - self._triangles[0::2, 2] = unique_vertices[3::4] + ng = self._nx * self._ny # grid size + self._triangles[0::2, 0] = unique_vertices[0:ng] + self._triangles[0::2, 1] = unique_vertices[2 * ng: 3 * ng] + self._triangles[0::2, 2] = unique_vertices[3 * ng: 4 * ng] # Split the quad cell into two triangular cells. - self._triangles[1::2, 0] = unique_vertices[3::4] - self._triangles[1::2, 1] = unique_vertices[1::4] - self._triangles[1::2, 2] = unique_vertices[0::4] + self._triangles[1::2, 0] = unique_vertices[3 * ng: 4 * ng] + self._triangles[1::2, 1] = unique_vertices[ng: 2 * ng] + self._triangles[1::2, 2] = unique_vertices[0:ng] # Each triangle cell is mapped to the tuple ID (ix, iy) of its parent mesh cell. - xm, ym = np.meshgrid(np.arange(self._nx, dtype=np.int32), np.arange(self._ny, dtype=np.int32), indexing='ij') - self._triangle_to_grid_map[::2, 0] = xm.flatten() - self._triangle_to_grid_map[::2, 1] = ym.flatten() - self._triangle_to_grid_map[1::2, :] = self._triangle_to_grid_map[::2, :] + ym, xm = np.meshgrid(np.arange(self._ny, dtype=np.int32), np.arange(self._nx, dtype=np.int32), indexing='ij') + self._triangle_to_grid_map[::2, 0] = ym.flatten() + self._triangle_to_grid_map[::2, 1] = xm.flatten() + self._triangle_to_grid_map[1::2] = self._triangle_to_grid_map[::2] tri_indices = np.arange(self._num_tris, dtype=np.int32) self._tri_index_loopup = Discrete2DMesh(self._vertex_coords, self._triangles, tri_indices) @property def nx(self): - """Number of grid cells in the x direction.""" + """Number of grid cells in the poloidal direction.""" return self._nx @property def ny(self): - """Number of grid cells in the y direction.""" + """Number of grid cells in the radial direction.""" return self._ny @property @@ -241,7 +242,7 @@ def poloidal_basis_vector(self): bx = (p_x r_x) ( b_p ) by (p_y r_y) ( b_r ) - :return: ndarray with shape (nx, ny, 2). + :return: ndarray with shape (2, ny, nx). """ return self._poloidal_basis_vector @@ -256,14 +257,14 @@ def radial_basis_vector(self): bx = (p_x r_x) ( b_p ) by (p_y r_y) ( b_r ) - :return: ndarray with shape (nx, ny, 2). + :return: ndarray with shape (2, ny, nx). """ return self._radial_basis_vector @property def triangle_to_grid_map(self): """ - Array mapping every triangle index to a tuple grid cell ID, i.e. (ix, iy). + Array mapping every triangle index to a tuple grid cell ID, i.e. (iy, ix). :return: ndarray with shape (nx*ny*2, 2) """ @@ -293,30 +294,28 @@ def __getstate__(self): def to_cartesian(self, vec_pol): """ Converts the 2D vector defined on mesh from poloidal to cartesian coordinates. - :param ndarray vec_pol: Array of 2D vector with with shape (nx, ny, 2). - [:, :, 0] - poloidal component, [:, :, 1] - radial component + :param ndarray vec_pol: Array of 2D vector with with shape (2, ny, nx). + [0, :, :] - poloidal component, [1, :, :] - radial component - :return: ndarray with shape (nx, ny, 2) + :return: ndarray with shape (2, ny, nx) """ - vec_cart = np.zeros((self._nx, self._ny, 2)) - vec_cart[:, :, 0] = self._poloidal_basis_vector[:, :, 0] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 0] * vec_pol[:, :, 1] - vec_cart[:, :, 1] = self._poloidal_basis_vector[:, :, 1] * vec_pol[:, :, 0] + self._radial_basis_vector[:, :, 1] * vec_pol[:, :, 1] + vec_cart = np.zeros((2, self._ny, self._nx)) + vec_cart[0] = self._poloidal_basis_vector[0] * vec_pol[0] + self._radial_basis_vector[0] * vec_pol[1] + vec_cart[1] = self._poloidal_basis_vector[1] * vec_pol[0] + self._radial_basis_vector[1] * vec_pol[1] return vec_cart def to_poloidal(self, vec_cart): """ Converts the 2D vector defined on mesh from cartesian to poloidal coordinates. - :param ndarray vector_on_mesh: Array of 2D vector with with shape (nx, ny, 2). - [:, :, 0] - R component, [:, :, 1] - Z component + :param ndarray vector_on_mesh: Array of 2D vector with with shape (2, ny, nx). + [0, :, :] - R component, [1, :, :] - Z component - :return: ndarray with shape (nx, ny, 2) + :return: ndarray with shape (2, ny, nx) """ - vec_pol = np.zeros((self._nx, self._ny, 2)) - vec_pol[:, :, 0] = self._inv_det * (self._radial_basis_vector[:, :, 1] * vec_cart[:, :, 0] - - self._radial_basis_vector[:, :, 0] * vec_cart[:, :, 1]) - vec_pol[:, :, 1] = self._inv_det * (self._poloidal_basis_vector[:, :, 0] * vec_cart[:, :, 1] - - self._poloidal_basis_vector[:, :, 1] * vec_cart[:, :, 0]) + vec_pol = np.zeros((2, self._ny, self._nx)) + vec_pol[0] = self._inv_det * (self._radial_basis_vector[1] * vec_cart[0] - self._radial_basis_vector[0] * vec_cart[1]) + vec_pol[1] = self._inv_det * (self._poloidal_basis_vector[0] * vec_cart[1] - self._poloidal_basis_vector[1] * vec_cart[0]) return vec_pol diff --git a/cherab/solps/solps_3d_functions.pyx b/cherab/solps/solps_3d_functions.pyx index c828ffc..4d8b68d 100755 --- a/cherab/solps/solps_3d_functions.pyx +++ b/cherab/solps/solps_3d_functions.pyx @@ -101,7 +101,9 @@ cdef class SOLPSVectorFunction3D(VectorFunction3D): # print(self._grid_vectors.shape) # Lookup vector for this grid cell. - vx, vy, vz = self._grid_vectors[ix, iy, :] + vx = self._grid_vectors[0, ix, iy] + vy = self._grid_vectors[1, ix, iy] + vz = self._grid_vectors[2, ix, iy] v = new_vector3d(vx, vy, vz) # Rotate vector field around the z-axis. diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 8eebc15..a3a8e89 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -39,19 +39,7 @@ from .mesh_geometry import SOLPSMesh -# TODO: This interface is half broken - some routines expect internal data as arrays, others as function 3d. -# -# In the future SOLPSSimulation should keep the data arrays in self._data dict, e.g. self._data["electron_temperature"], etc. -# Method self.set_electon_temperature(value) should set self._data["electron_temperature"] to value and -# initialise self._electron_temperature as a Discrete2DMesh (or AxisymmetricMapper, or SOLPSFunction3D), etc. -# Method self.get_electon_temperature() should return np.copy(self._data["electron_temperature"]), while -# self.electron_temperature should return a Discrete2DMesh (or AxisymmetricMapper, or SOLPSFunction3D) instance, etc. -# -# As an option: self.electron_temperature returns a Discrete2DMesh instance, self.electron_temperature_3d returns -# AxisymmetricMapper or SOLPSFunction3D (?). -# -# Also, there is some confusion in coordinates system names. We have curvilinear poloidal coordinates (poloidal, radial, toroidal), -# plane toroidal coordinates (R, Z, toroidal) and Cartesian coordinates (x, y, z). For now toroidal coordinates refered as Cartesian. +# TODO: Implement *_as_f2d() and *_as_f3d() interpolators for plasma parameters class SOLPSSimulation: @@ -79,12 +67,12 @@ def __init__(self, mesh, species_list): self._species_density = None self._velocities = None self._velocities_cartesian = None - self._total_rad = None + self._total_radiation = None self._b_field_vectors = None self._b_field_vectors_cartesian = None - self._eirene_model = None - self._b2_model = None - self._eirene = None + self._eirene_model = None # what is this for? + self._b2_model = None # what is this for? + self._eirene = None # do we need this in SOLPSSimulation? @property def mesh(self): @@ -110,12 +98,9 @@ def electron_temperature(self): """ return self._electron_temperature - def get_electron_temperature(self): - - return self._electron_temperature - - def set_electron_temperature(self, value): - _check_array("electron_temperature", value, (self.mesh.nx, self.mesh.ny)) + @electron_temperature.setter + def electron_temperature(self, value): + _check_array("electron_temperature", value, (self.mesh.ny, self.mesh.nx)) self._electron_temperature = value @@ -127,12 +112,9 @@ def ion_temperature(self): """ return self._ion_temperature - def get_ion_temperature(self): - - return self._ion_temperature - - def set_ion_temperature(self, value): - _check_array("ion_temperature", value, (self.mesh.nx, self.mesh.ny)) + @ion_temperature.setter + def ion_temperature(self, value): + _check_array("ion_temperature", value, (self.mesh.ny, self.mesh.nx)) self._ion_temperature = value @@ -144,13 +126,10 @@ def neutral_temperature(self): """ return self._neutral_temperature - def get_neutral_temperature(self): - - return self._neutral_temperature - - def set_neutral_temperature(self, value): + @neutral_temperature.setter + def neutral_temperature(self, value): num_neutrals = len([sp for sp in self.species_list if sp[1] == 0]) - _check_array("neutral_temperature", value, (self.mesh.nx, self.mesh.ny, num_neutrals)) + _check_array("neutral_temperature", value, (num_neutrals, self.mesh.ny, self.mesh.nx)) self._neutral_temperature = value @@ -162,12 +141,9 @@ def electron_density(self): """ return self._electron_density - def get_electron_density(self): - - return self._electron_density - - def set_electron_density(self, value): - _check_array("electron_density", value, (self.mesh.nx, self.mesh.ny)) + @electron_density.setter + def electron_density(self, value): + _check_array("electron_density", value, (self.mesh.ny, self.mesh.nx)) self._electron_density = value @@ -179,12 +155,9 @@ def species_density(self): """ return self._species_density - def get_species_density(self): - - return self._species_density - - def set_species_density(self, value): - _check_array("species_density", value, (self.mesh.nx, self.mesh.ny, len(self.species_list))) + @species_density.setter + def species_density(self, value): + _check_array("species_density", value, (len(self.species_list), self.mesh.ny, self.mesh.nx)) self._species_density = value @@ -196,18 +169,15 @@ def velocities(self): """ return self._velocities - def get_velocities(self): - - return self._velocities - - def set_velocities(self, value): - _check_array("velocities", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) + @velocities.setter + def velocities(self, value): + _check_array("velocities", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) # Converting to Cartesian coordinates velocities_cartesian = np.zeros(value.shape) - velocities_cartesian[:, :, :, 2] = value[:, :, :, 2] - for k in range(value.shape[2]): - velocities_cartesian[:, :, k, :2] = self.mesh.to_cartesian(value[:, :, k, :2]) + velocities_cartesian[:, 2] = value[:, 2] + for k in range(value.shape[0]): + velocities_cartesian[k, :2] = self.mesh.to_cartesian(value[k, :2]) self._velocities_cartesian = velocities_cartesian self._velocities = value @@ -218,21 +188,17 @@ def velocities_cartesian(self): Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. :return: """ - # TODO: If converted to SOLPSVectorFunction3D, should porbably return at (vx, vy, vz) at (x, y, z). (?) - return self._velocities_cartesian - - def get_velocities_cartesian(self): - return self._velocities_cartesian - def set_velocities_cartesian(self, value): - _check_array("velocities_cartesian", value, (self.mesh.nx, self.mesh.ny, len(self.species_list), 3)) + @velocities_cartesian.setter + def velocities_cartesian(self, value): + _check_array("velocities_cartesian", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) # Converting to poloidal coordinates velocities = np.zeros(value.shape) - velocities[:, :, :, 2] = value[:, :, :, 2] - for k in range(value.shape[2]): - velocities[:, :, k, :2] = self.mesh.to_poloidal(value[:, :, k, :2]) + velocities[:, 2] = value[:, 2] + for k in range(value.shape[0]): + velocities[k, :2] = self.mesh.to_poloidal(value[k, :2]) self._velocities = value self._velocities_cartesian = value @@ -256,19 +222,16 @@ def total_radiation(self): Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' and 'RQBRM'. Final output is in W/str? """ - if self._total_rad is None: + if self._total_radiation is None: raise RuntimeError("Total radiation not available for this simulation.") else: - return self._total_rad + return self._total_radiation - def get_total_radiation(self): + @total_radiation.setter + def total_radiation(self, value): + _check_array("total_radiation", value, (self.mesh.ny, self.mesh.nx)) - return self._total_rad - - def set_total_radiation(self, value): - _check_array("total_radiation", value, (self.mesh.nx, self.mesh.ny)) - - self._total_rad = value + self._total_radiation = value # TODO: decide is this a 2D or 3D interface? @property @@ -293,51 +256,44 @@ def b_field(self): """ Magnetic B field at each mesh cell in mesh cell coordinates (b_poloidal, b_radial, b_toroidal). """ - if self._b_field_vectors is None: + if self._b_field is None: raise RuntimeError("Magnetic field not available for this simulation.") else: - return self._b_field_vectors - - def get_b_field(self): - - return self._b_field_vectors + return self._b_field - def set_b_field(self, value): - _check_array("b_field", value, (self.mesh.nx, self.mesh.ny, 3)) + @b_field.setter + def b_field(self, value): + _check_array("b_field", value, (3, self.mesh.ny, self.mesh.nx)) # Converting to cartesian system - b_field_vectors_cartesian = np.zeros(value.shape) - b_field_vectors_cartesian[:, :, 2] = value[:, :, 2] - b_field_vectors_cartesian[:, :, :2] = self.mesh.to_cartesian(value[:, :, :2]) + b_field_cartesian = np.zeros(value.shape) + b_field_cartesian[2] = value[2] + b_field_cartesian[:2] = self.mesh.to_cartesian(value[:2]) - self._b_field_vectors_cartesian = b_field_vectors_cartesian - self._b_field_vectors = value + self._b_field_cartesian = b_field_cartesian + self._b_field = value @property def b_field_cartesian(self): """ Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). """ - # TODO: If converted to SOLPSVectorFunction3D, should porbably return at (Bx, By, Bz) at (x, y, z). (?) - if self._b_field_vectors_cartesian is None: + if self._b_field_cartesian is None: raise RuntimeError("Magnetic field not available for this simulation.") else: - return self._b_field_vectors_cartesian + return self._b_field_cartesian - def get_b_field_cartesian(self): - - return self._b_field_vectors_cartesian - - def set_b_field_cartesian(self, value): - _check_array("b_field_cartesian", value, (self.mesh.nx, self.mesh.ny, 3)) + @b_field_cartesian.setter + def b_field_cartesian(self, value): + _check_array("b_field_cartesian", value, (3, self.mesh.ny, self.mesh.nx)) # Converting to poloidal system - b_field_vectors = np.zeros(value.shape) - b_field_vectors[:, :, 2] = value[:, :, 2] - b_field_vectors[:, :, :2] = self.mesh.to_poloidal(value[:, :, :2]) + b_field = np.zeros(value.shape) + b_field[2] = value[2] + b_field[:2] = self.mesh.to_poloidal(value[:2]) - self._b_field_vectors = b_field_vectors - self._b_field_vectors_cartesian = value + self._b_field = b_field + self._b_field_cartesian = value @property def eirene_simulation(self): @@ -370,9 +326,9 @@ def __getstate__(self): 'velocities': self._velocities, 'velocities_cartesian': self._velocities_cartesian, 'inside_mesh': self._inside_mesh, - 'total_rad': self._total_rad, - 'b_field_vectors': self._b_field_vectors, - 'b_field_vectors_cartesian': self._b_field_vectors_cartesian, + 'total_radiation': self._total_radiation, + 'b_field': self._b_field, + 'b_field_cartesian': self._b_field_cartesian, 'eirene_model': self._eirene_model, 'b2_model': self._b2_model, 'eirene': self._eirene @@ -390,9 +346,9 @@ def __setstate__(self, state): self._velocities = state['velocities'] self._velocities_cartesian = state['velocities_cartesian'] self._inside_mesh = state['inside_mesh'] - self._total_rad = state['total_rad'] - self._b_field_vectors = state['b_field_vectors'] - self._b_field_vectors_cartesian = state['b_field_vectors_cartesian'] + self._total_radiation = state['total_radiation'] + self._b_field = state['b_field'] + self._b_field_cartesian = state['b_field_cartesian'] self._eirene_model = state['eirene_model'] self._b2_model = state['b2_model'] self._eirene = state['eirene'] @@ -560,28 +516,27 @@ def create_plasma(self, parent=None, transform=None, name=None): tri_to_grid = self.mesh.triangle_to_grid_map try: - plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.get_b_field_cartesian()) - # self.get_.. always returns data even if self.b_field_cartesian will return function in the future + plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.b_field_cartesian) except RuntimeError: print('Warning! No magnetic field data available for this simulation.') # Create electron species - triangle_data = _map_data_onto_triangles(self.get_electron_temperature()) + triangle_data = _map_data_onto_triangles(self.electron_temperature) electron_te_interp = Discrete2DMesh(mesh.vertex_coordinates, mesh.triangles, triangle_data, limit=False) electron_temp = AxisymmetricMapper(electron_te_interp) - triangle_data = _map_data_onto_triangles(self.get_electron_density()) + triangle_data = _map_data_onto_triangles(self.electron_density) electron_dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) plasma.electron_distribution = Maxwellian(electron_dens, electron_temp, electron_velocity, electron_mass) # Ion temperature - triangle_data = _map_data_onto_triangles(self.get_ion_temperature()) + triangle_data = _map_data_onto_triangles(self.ion_temperature) ion_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - if self.get_velocities_cartesian() is None: + if self.velocities_cartesian is None: print('Warning! No velocity field data available for this simulation.') - if self.get_neutral_temperature() is None: + if self.neutral_temperature is None: print('Warning! No neutral atom temperature data available for this simulation.') neutral_i = 0 # neutrals count @@ -594,22 +549,22 @@ def create_plasma(self, parent=None, transform=None, name=None): charge = sp[1] - triangle_data = _map_data_onto_triangles(self.get_species_density()[:, :, k]) + triangle_data = _map_data_onto_triangles(self.species_density[k]) dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, self.species_density[:, :, k]) + # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, self.species_density[k]) # Create the velocity vector lookup function - if self.get_velocities_cartesian() is not None: - velocity = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.get_velocities_cartesian()[:, :, k, :]) + if self.velocities_cartesian is not None: + velocity = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.velocities_cartesian[k]) else: velocity = lambda x, y, z: Vector3D(0, 0, 0) - if charge or self.get_neutral_temperature() is None: # ions or neutral atoms (neutral temperature is not available) + if charge or self.neutral_temperature is None: # ions or neutral atoms (neutral temperature is not available) distribution = Maxwellian(dens, ion_temp, velocity, species_type.atomic_weight * atomic_mass) else: # neutral atoms with neutral temperature - triangle_data = _map_data_onto_triangles(self.get_neutral_temperature()[:, :, neutral_i]) + triangle_data = _map_data_onto_triangles(self.neutral_temperature[neutral_i]) neutral_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) distribution = Maxwellian(dens, neutral_temp, velocity, species_type.atomic_weight * atomic_mass) neutral_i += 1 @@ -659,81 +614,81 @@ def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velo :param SOLPSMesh mesh: SOLPS simulation mesh. :param ndarray density: Density of atoms in m-3. Must be 3 dimensiona array of - shape (mesh.nx, mesh.ny, num_atoms). + shape (num_atoms, mesh.ny, mesh.nx). :param ndarray poloidal_flux: Poloidal flux of atoms in s-1. Must be a 3 dimensional array of - shape (mesh.nx, mesh.ny, num_atoms). + shape (num_atoms, mesh.ny, mesh.nx). :param ndarray radial_flux: Radial flux of atoms in s-1. Must be a 3 dimensional array of - shape (mesh.nx, mesh.ny, num_atoms). + shape (num_atoms, mesh.ny, mesh.nx). :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional - array of shape (mesh.nx, mesh.ny, num_atoms). + array of shape (num_atoms, mesh.ny, mesh.nx). Parallel velocity is a velocity projection on magnetic field direction. :param ndarray b_field_cartesian: Magnetic field in Cartesian (R, Z, phi) coordinates. - Must be a 3 dimensional array of shape (mesh.nx, mesh.ny, 3). + Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of - shape (mesh.nx, mesh.ny, num_atoms, 3) + shape (num_atoms, 3, mesh.ny, mesh.nx) """ - nx = mesh.nx - ny = mesh.ny - ns = density.shape[2] - - _check_array('density', poloidal_flux, (nx, ny, ns)) - _check_array('poloidal_flux', poloidal_flux, (nx, ny, ns)) - _check_array('radial_flux', radial_flux, (nx, ny, ns)) - _check_array('parallel_velocity', parallel_velocity, (nx, ny, ns)) - _check_array('b_field_cartesian', b_field_cartesian, (nx, ny, 3)) - - poloidal_area = mesh.poloidal_area[:, :, None] - radial_area = mesh.radial_area[:, :, None] - leftix = mesh.neighbix[:, :, 0] # poloidal prev. - leftiy = mesh.neighbiy[:, :, 0] - bottomix = mesh.neighbix[:, :, 1] # radial prev. - bottomiy = mesh.neighbiy[:, :, 1] - rightix = mesh.neighbix[:, :, 2] # poloidal next. - rightiy = mesh.neighbiy[:, :, 2] - topix = mesh.neighbix[:, :, 3] # radial next. - topiy = mesh.neighbiy[:, :, 3] + nx = mesh.nx # poloidal + ny = mesh.ny # radial + ns = density.shape[0] # number of species + + _check_array('density', density, (ns, ny, nx)) + _check_array('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_array('radial_flux', radial_flux, (ns, ny, nx)) + _check_array('parallel_velocity', parallel_velocity, (ns, ny, nx)) + _check_array('b_field_cartesian', b_field_cartesian, (3, ny, nx)) + + poloidal_area = mesh.poloidal_area[None] + radial_area = mesh.radial_area[None] + leftix = mesh.neighbix[0] # poloidal prev. + leftiy = mesh.neighbiy[0] + bottomix = mesh.neighbix[1] # radial prev. + bottomiy = mesh.neighbiy[1] + rightix = mesh.neighbix[2] # poloidal next. + rightiy = mesh.neighbiy[2] + topix = mesh.neighbix[3] # radial next. + topiy = mesh.neighbiy[3] # Converting s-1 --> m-2 s-1 poloidal_flux = np.divide(poloidal_flux, poloidal_area, out=np.zeros_like(poloidal_flux), where=poloidal_area > 0) radial_flux = np.divide(radial_flux, radial_area, out=np.zeros_like(radial_flux), where=radial_area > 0) # Obtaining left velocity - dens_neighb = density[leftix, leftiy, :] # density in the left neighbouring cell - has_neighbour = ((leftix > -1) * (leftiy > -1))[:, :, None] # check if has left neighbour + dens_neighb = density[:, leftiy, leftix] # density in the left neighbouring cell + has_neighbour = ((leftix > -1) * (leftiy > -1))[None] # check if has left neighbour neg_flux = (poloidal_flux < 0) * (density > 0) # will use density in this cell if flux is negative pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour # will use density in neighbouring cell if flux is positive - velocity_left = np.divide(poloidal_flux, density, out=np.zeros((nx, ny, ns)), where=neg_flux) + velocity_left = np.divide(poloidal_flux, density, out=np.zeros((ns, ny, nx)), where=neg_flux) velocity_left = np.divide(poloidal_flux, dens_neighb, out=velocity_left, where=pos_flux) - velocity_left = velocity_left[:, :, :, None] * mesh.poloidal_basis_vector[:, :, None, :] # to vector in Cartesian + velocity_left = velocity_left[:, None] * mesh.poloidal_basis_vector[None] # to vector in Cartesian # Obtaining bottom velocity - dens_neighb = density[bottomix, bottomiy, :] - has_neighbour = ((bottomix > -1) * (bottomiy > -1))[:, :, None] + dens_neighb = density[:, bottomiy, bottomix] + has_neighbour = ((bottomix > -1) * (bottomiy > -1))[None] neg_flux = (radial_flux < 0) * (density > 0) pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour - velocity_bottom = np.divide(radial_flux, density, out=np.zeros((nx, ny, ns)), where=neg_flux) + velocity_bottom = np.divide(radial_flux, density, out=np.zeros((ns, ny, nx)), where=neg_flux) velocity_bottom = np.divide(radial_flux, dens_neighb, out=velocity_bottom, where=pos_flux) - velocity_bottom = velocity_bottom[:, :, :, None] * mesh.radial_basis_vector[:, :, None, :] # to Cartesian + velocity_bottom = velocity_bottom[:, None] * mesh.radial_basis_vector[None] # to Cartesian # Obtaining right and top velocities - velocity_right = velocity_left[rightix, rightiy, :, :] - velocity_right[(rightix < 0) + (rightiy < 0)] = 0 + velocity_right = velocity_left[:, :, rightiy, rightix] + velocity_right[:, :, (rightix < 0) + (rightiy < 0)] = 0 - velocity_top = velocity_bottom[topix, topiy, :, :] - velocity_top[(topix < 0) + (topiy < 0)] = 0 + velocity_top = velocity_bottom[:, :, topiy, topix] + velocity_top[:, :, (topix < 0) + (topiy < 0)] = 0 - vcart = np.zeros((nx, ny, ns, 3)) # velocities in Cartesian coordinates + vcart = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates # Projection of velocity on RZ-plane - vcart[:, :, :, :2] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) + vcart[:, :2] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) # Obtaining toroidal velocity - b = b_field_cartesian[:, :, None, :] - bmagn = np.sqrt((b * b).sum(3)) - vcart[:, :, :, 2] = (parallel_velocity * bmagn - vcart[:, :, :, 0] * b[:, :, :, 0] - vcart[:, :, :, 1] * b[:, :, :, 1]) / b[:, :, :, 2] + b = b_field_cartesian[None] + bmagn = np.sqrt((b * b).sum(1)) + vcart[:, 2] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 1] * b[:, 1]) / b[:, 2] return vcart @@ -744,45 +699,44 @@ def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_ :param SOLPSMesh mesh: SOLPS simulation mesh. :param ndarray density: Density of atoms in m-3. Must be 3 dimensiona array of - shape (mesh.nx, mesh.ny, num_atoms). + shape (num_atoms, mesh.ny, mesh.nx). :param ndarray poloidal_flux: Poloidal flux of atoms in m-2 s-1. Must be a 3 dimensional array of - shape (mesh.nx, mesh.ny, num_atoms). + shape (num_atoms, mesh.ny, mesh.nx). :param ndarray radial_flux: Radial flux of atoms in m-2 s-1. Must be a 3 dimensional array of - shape (mesh.nx, mesh.ny, num_atoms). + shape (num_atoms, mesh.ny, mesh.nx). :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional - array of shape (mesh.nx, mesh.ny, num_atoms). + array of shape (num_atoms, mesh.ny, mesh.nx). Parallel velocity is a velocity projection on magnetic field direction. :param ndarray b_field_cartesian: Magnetic field in Cartesian (R, Z, phi) coordinates. - Must be a 3 dimensional array of shape (mesh.nx, mesh.ny, 3). + Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of shape (mesh.nx, mesh.ny, num_atoms, 3) """ - nx = mesh.nx - ny = mesh.ny - ns = density.shape[2] + nx = mesh.nx # poloidal + ny = mesh.ny # radial + ns = density.shape[0] # number of neutral atoms - _check_array('density', poloidal_flux, (nx, ny, ns)) - _check_array('poloidal_flux', poloidal_flux, (nx, ny, ns)) - _check_array('radial_flux', radial_flux, (nx, ny, ns)) - _check_array('parallel_velocity', parallel_velocity, (nx, ny, ns)) - _check_array('b_field_cartesian', b_field_cartesian, (nx, ny, 3)) + _check_array('density', density, (ns, ny, nx)) + _check_array('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_array('radial_flux', radial_flux, (ns, ny, nx)) + _check_array('parallel_velocity', parallel_velocity, (ns, ny, nx)) + _check_array('b_field_cartesian', b_field_cartesian, (3, ny, nx)) # Obtaining velocity - poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros((nx, ny, ns)), where=(density > 0)) - radial_velocity = np.divide(radial_flux, density, out=np.zeros((nx, ny, ns)), where=(density > 0)) + poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) + radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) - vcart = np.zeros((nx, ny, ns, 3)) # velocities in Cartesian coordinates + vcart = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates # Projection of velocity on RZ-plane - vcart[:, :, :, :2] = (poloidal_velocity[:, :, :, None] * mesh.poloidal_basis_vector[:, :, None, :] + - radial_velocity[:, :, :, None] * mesh.radial_basis_vector[:, :, None, :]) # to vector in Cartesian + vcart[:, :2] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector[None] + radial_velocity[:, None] * mesh.radial_basis_vector[None]) # Obtaining toroidal velocity - b = b_field_cartesian[:, :, None, :] - bmagn = np.sqrt((b * b).sum(3)) - vcart[:, :, :, 2] = (parallel_velocity * bmagn - (vcart[:, :, :, 0] * b[:, :, :, 0] + vcart[:, :, :, 1] * b[:, :, :, 1])) / b[:, :, :, 2] + b = b_field_cartesian[None] + bmagn = np.sqrt((b * b).sum(1)) + vcart[:, 2] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 1] * b[:, 1]) / b[:, 2] return vcart From 8953c542c22114461c93cc0fc6e945aa9744e690 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Fri, 11 Sep 2020 02:33:55 +0300 Subject: [PATCH 17/25] Implemented SOLPSFunction2D and SOLPSVectorFunction2D, added *_f2d() and *_f3d() interpolators. --- cherab/solps/__init__.py | 2 +- cherab/solps/mesh_geometry.py | 23 +- cherab/solps/models/radiated_power.pyx | 2 +- cherab/solps/solps_2d_functions.pyx | 206 ++++++++++++++ cherab/solps/solps_plasma.py | 368 ++++++++++++++++++------- 5 files changed, 490 insertions(+), 111 deletions(-) create mode 100755 cherab/solps/solps_2d_functions.pyx diff --git a/cherab/solps/__init__.py b/cherab/solps/__init__.py index 03d35fc..e53b2db 100644 --- a/cherab/solps/__init__.py +++ b/cherab/solps/__init__.py @@ -17,7 +17,7 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -from .solps_3d_functions import SOLPSFunction3D, SOLPSVectorFunction3D +from .solps_2d_functions import SOLPSFunction2D, SOLPSVectorFunction2D from .mesh_geometry import SOLPSMesh from .solps_plasma import SOLPSSimulation from .formats import load_solps_from_raw_output, load_solps_from_mdsplus, load_solps_from_balance diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index e261a51..b4dd30f 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -24,10 +24,6 @@ import matplotlib.pyplot as plt from matplotlib.patches import Polygon from matplotlib.collections import PatchCollection -from raysect.core.math.function.float import Discrete2DMesh - -# INFINITY = 1E99 - class SOLPSMesh: """ @@ -148,9 +144,6 @@ def __init__(self, r, z, vol, neighbix, neighbiy): self._triangle_to_grid_map[::2, 1] = xm.flatten() self._triangle_to_grid_map[1::2] = self._triangle_to_grid_map[::2] - tri_indices = np.arange(self._num_tris, dtype=np.int32) - self._tri_index_loopup = Discrete2DMesh(self._vertex_coords, self._triangles, tri_indices) - @property def nx(self): """Number of grid cells in the poloidal direction.""" @@ -270,16 +263,16 @@ def triangle_to_grid_map(self): """ return self._triangle_to_grid_map - @property - def triangle_index_lookup(self): - """ - Discrete2DMesh instance that looks up a triangle index at any 2D point. + # @property + # def triangle_index_lookup(self): + # """ + # Discrete2DMesh instance that looks up a triangle index at any 2D point. - Useful for mapping from a 2D point -> triangle cell -> parent SOLPS mesh cell + # Useful for mapping from a 2D point -> triangle cell -> parent SOLPS mesh cell - :return: Discrete2DMesh instance - """ - return self._tri_index_loopup + # :return: Discrete2DMesh instance + # """ + # return self._tri_index_loopup def __getstate__(self): state = { diff --git a/cherab/solps/models/radiated_power.pyx b/cherab/solps/models/radiated_power.pyx index fa4f0d3..72ae0f8 100755 --- a/cherab/solps/models/radiated_power.pyx +++ b/cherab/solps/models/radiated_power.pyx @@ -35,7 +35,7 @@ cdef class SOLPSTotalRadiatedPower(InhomogeneousVolumeEmitter): self.vertical_offset = vertical_offset self.inside_simulation = solps_simulation.inside_volume_mesh - self.total_rad = solps_simulation.total_radiation_volume + self.total_rad = solps_simulation.total_radiation_f3d def __call__(self, x, y , z): diff --git a/cherab/solps/solps_2d_functions.pyx b/cherab/solps/solps_2d_functions.pyx new file mode 100755 index 0000000..6088878 --- /dev/null +++ b/cherab/solps/solps_2d_functions.pyx @@ -0,0 +1,206 @@ +# cython: language_level=3 + +# Copyright 2016-2018 Euratom +# Copyright 2016-2018 United Kingdom Atomic Energy Authority +# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import numpy as np +cimport numpy as np + +from raysect.core.math.function.float.function2d.interpolate.common cimport MeshKDTree2D +from raysect.core.math.vector cimport Vector3D, new_vector3d +from raysect.core.math.point cimport new_point2d + +from cherab.core.math.function cimport Function2D, VectorFunction2D +cimport cython + + +cdef class SOLPSFunction2D(Function2D): + + cdef: + MeshKDTree2D _kdtree + np.ndarray _grid_data, _triangle_to_grid_map + np.int32_t[:,::1] _triangle_to_grid_map_mv + double[:,::1] _grid_data_mv + + def __init__(self, object vertex_coords not None, object triangles not None, object triangle_to_grid_map not None, object grid_data not None): + + # use numpy arrays to store data internally + vertex_coords = np.array(vertex_coords, dtype=np.float64) + triangles = np.array(triangles, dtype=np.int32) + + # build kdtree + self._kdtree = MeshKDTree2D(vertex_coords, triangles) + + # populate internal attributes + self._grid_data = grid_data + self._triangle_to_grid_map = triangle_to_grid_map + + self._grid_data_mv = self._grid_data + self._triangle_to_grid_map_mv = self._triangle_to_grid_map + + def __getstate__(self): + return self._grid_data, self._triangle_to_grid_map, self._kdtree + + def __setstate__(self, state): + self._grid_data, self._triangle_to_grid_map, self._kdtree = state + self._triangle_to_grid_map_mv = self._triangle_to_grid_map + self._grid_data_mv = self._grid_data + + def __reduce__(self): + return self.__new__, (self.__class__, ), self.__getstate__() + + @classmethod + def instance(cls, SOLPSFunction2D instance not None, object grid_data=None): + """ + Creates a new interpolator instance from an existing interpolator instance. + The new interpolator instance will share the same internal acceleration + data as the original interpolator. The grid_data of the new instance can + be redefined. + This method should be used if the user has multiple sets of grid_data + that lie on the same mesh geometry. Using this methods avoids the + repeated rebuilding of the mesh acceleration structures by sharing the + geometry data between multiple interpolator objects. + :param SOLPSFunction2D instance: SOLPSFunction2D object. + :param ndarray grid_data: An array containing data on SOLPS grid. + :return: A SOLPSFunction2D object. + :rtype: SOLPSFunction2D + """ + + cdef SOLPSFunction2D m + + # copy source data + m = SOLPSFunction2D.__new__(SOLPSFunction2D) + m._kdtree = instance._kdtree + m._triangle_to_grid_map = instance._triangle_to_grid_map + + # do we have replacement triangle data? + if grid_data is None: + m._grid_data = instance._grid_data + else: + m._grid_data = grid_data + + m._triangle_to_grid_map_mv = m._triangle_to_grid_map + m._grid_data_mv = m._grid_data + + return m + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + cdef double evaluate(self, double x, double y) except? -1e999: + + cdef: + np.int32_t triangle_id, ix, iy + + if self._kdtree.is_contained(new_point2d(x, y)): + + triangle_id = self._kdtree.triangle_id + ix, iy = self._triangle_to_grid_map_mv[triangle_id, :] + return self._grid_data_mv[ix, iy] + + return 0.0 + +cdef class SOLPSVectorFunction2D(VectorFunction2D): + + cdef: + MeshKDTree2D _kdtree + np.ndarray _grid_vectors, _triangle_to_grid_map + np.int32_t[:,::1] _triangle_to_grid_map_mv + double[:,:,::1] _grid_vectors_mv + + def __init__(self, object vertex_coords not None, object triangles not None, object triangle_to_grid_map not None, object grid_vectors not None): + + # use numpy arrays to store data internally + vertex_coords = np.array(vertex_coords, dtype=np.float64) + triangles = np.array(triangles, dtype=np.int32) + + # build kdtree + self._kdtree = MeshKDTree2D(vertex_coords, triangles) + + # populate internal attributes + self._grid_vectors = grid_vectors + self._triangle_to_grid_map = triangle_to_grid_map + self._grid_vectors_mv = self._grid_vectors + self._triangle_to_grid_map_mv = self._triangle_to_grid_map + + def __getstate__(self): + return self._grid_vectors, self._triangle_to_grid_map, self._kdtree + + def __setstate__(self, state): + self._grid_vectors, self._triangle_to_grid_map, self._kdtree = state + self._grid_vectors_mv = self._grid_vectors + self._triangle_to_grid_map_mv = self._triangle_to_grid_map + + def __reduce__(self): + return self.__new__, (self.__class__, ), self.__getstate__() + + @classmethod + def instance(cls, SOLPSVectorFunction2D instance not None, object grid_vectors=None): + """ + Creates a new interpolator instance from an existing interpolator instance. + The new interpolator instance will share the same internal acceleration + data as the original interpolator. The grid_data of the new instance can + be redefined. + This method should be used if the user has multiple sets of grid_data + that lie on the same mesh geometry. Using this methods avoids the + repeated rebuilding of the mesh acceleration structures by sharing the + geometry data between multiple interpolator objects. + :param SOLPSVectorFunction2D instance: SOLPSVectorFunction2D object. + :param ndarray grid_data: An array containing data on SOLPS grid. + :return: A SOLPSVectorFunction2D object. + :rtype: SOLPSVectorFunction2D + """ + + cdef SOLPSVectorFunction2D m + + # copy source data + m = SOLPSVectorFunction2D.__new__(SOLPSVectorFunction2D) + m._kdtree = instance._kdtree + m._triangle_to_grid_map = instance._triangle_to_grid_map + + # do we have replacement triangle data? + if grid_vectors is None: + m._grid_vectors = instance._grid_vectors + else: + m._grid_vectors = grid_vectors + + m._triangle_to_grid_map_mv = m._triangle_to_grid_map + m._grid_vectors_mv = m._grid_vectors + + return m + + @cython.boundscheck(False) + @cython.wraparound(False) + @cython.initializedcheck(False) + cdef Vector3D evaluate(self, double x, double y): + + cdef: + np.int32_t triangle_id, ix, iy + double vx, vy, vz + + if self._kdtree.is_contained(new_point2d(x, y)): + + triangle_id = self._kdtree.triangle_id + ix, iy = self._triangle_to_grid_map_mv[triangle_id, :] + vx = self._grid_vectors_mv[0, ix, iy] + vy = self._grid_vectors_mv[1, ix, iy] + vz = self._grid_vectors_mv[2, ix, iy] + + return new_vector3d(vx, vy, vz) + + return new_vector3d(0, 0, 0) diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index a3a8e89..6b36b55 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -23,24 +23,21 @@ from scipy.constants import atomic_mass, electron_mass # Raysect imports -from raysect.core.math.function.float import Discrete2DMesh from raysect.core import translate, Point3D, Vector3D, Node, AffineMatrix3D from raysect.primitive import Cylinder from raysect.optical import Spectrum # CHERAB core imports from cherab.core import Plasma, Species, Maxwellian -from cherab.core.math.mappers import AxisymmetricMapper +from cherab.core.math.mappers import AxisymmetricMapper, VectorAxisymmetricMapper from cherab.core.atomic.elements import lookup_isotope, lookup_element # This SOLPS package imports from cherab.solps.eirene import Eirene -from .solps_3d_functions import SOLPSFunction3D, SOLPSVectorFunction3D +from .solps_2d_functions import SOLPSFunction2D, SOLPSVectorFunction2D from .mesh_geometry import SOLPSMesh -# TODO: Implement *_as_f2d() and *_as_f3d() interpolators for plasma parameters - class SOLPSSimulation: def __init__(self, mesh, species_list): @@ -52,24 +49,44 @@ def __init__(self, mesh, species_list): raise ValueError('Argument "mesh" must be a SOLPSMesh instance.') # Make Mesh Interpolator function for inside/outside mesh test. - inside_outside_data = np.ones(mesh.num_triangles) - inside_outside = AxisymmetricMapper(Discrete2DMesh(mesh.vertex_coordinates, mesh.triangles, inside_outside_data, limit=False)) - self._inside_mesh = inside_outside + inside_outside_data = np.ones((self._mesh.ny, self._mesh.nx)) + self._inside_mesh = SOLPSFunction2D(mesh.vertex_coordinates, mesh.triangles, mesh.triangle_to_grid_map, inside_outside_data) + + # Creating a sample SOLPSVectorFunction2D for KDtree to use later + sample_vector = np.ones((3, self._mesh.ny, self._mesh.nx)) + self._sample_vector_f2d = SOLPSVectorFunction2D(mesh.vertex_coordinates, mesh.triangles, mesh.triangle_to_grid_map, sample_vector) if not len(species_list): raise ValueError('Argument "species_list" must contain at least one species.') self._species_list = tuple(species_list) # adding additional species is not allowed + self._neutral_list = tuple([sp for sp in self._species_list if sp[1] == 0]) self._electron_temperature = None + self._electron_temperature_f2d = None + self._electron_temperature_f3d = None self._electron_density = None + self._electron_density_f2d = None + self._electron_density_f3d = None self._ion_temperature = None + self._ion_temperature_f2d = None + self._ion_temperature_f3d = None self._neutral_temperature = None + self._neutral_temperature_f2d = None + self._neutral_temperature_f3d = None self._species_density = None + self._species_density_f2d = None + self._species_density_f3d = None self._velocities = None self._velocities_cartesian = None + self._velocities_cartesian_f2d = None + self._velocities_cartesian_f3d = None self._total_radiation = None - self._b_field_vectors = None - self._b_field_vectors_cartesian = None + self._total_radiation_f2d = None + self._total_radiation_f3d = None + self._b_field = None + self._b_field_cartesian = None + self._b_field_cartesian_f2d = None + self._b_field_cartesian_f3d = None self._eirene_model = None # what is this for? self._b2_model = None # what is this for? self._eirene = None # do we need this in SOLPSSimulation? @@ -98,11 +115,28 @@ def electron_temperature(self): """ return self._electron_temperature + @property + def electron_temperature_f2d(self): + """ + Simulated electron temperatures at each mesh cell. + :return: + """ + return self._electron_temperature_f2d + + @property + def electron_temperature_f3d(self): + """ + Simulated electron temperatures at each mesh cell. + :return: + """ + return self._electron_temperature_f3d + @electron_temperature.setter def electron_temperature(self, value): _check_array("electron_temperature", value, (self.mesh.ny, self.mesh.nx)) - self._electron_temperature = value + self._electron_temperature_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) + self._electron_temperature_f3d = AxisymmetricMapper(self._electron_temperature_f2d) @property def ion_temperature(self): @@ -112,11 +146,28 @@ def ion_temperature(self): """ return self._ion_temperature + @property + def ion_temperature_f2d(self): + """ + Simulated ion temperatures at each mesh cell. + :return: + """ + return self._ion_temperature_f2d + + @property + def ion_temperature_f3d(self): + """ + Simulated ion temperatures at each mesh cell. + :return: + """ + return self._ion_temperature_f3d + @ion_temperature.setter def ion_temperature(self, value): _check_array("ion_temperature", value, (self.mesh.ny, self.mesh.nx)) - self._ion_temperature = value + self._ion_temperature_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) + self._ion_temperature_f3d = AxisymmetricMapper(self._ion_temperature_f2d) @property def neutral_temperature(self): @@ -126,12 +177,33 @@ def neutral_temperature(self): """ return self._neutral_temperature + @property + def neutral_temperature_f2d(self): + """ + Array of neutral atom (effective) temperature at each mesh cell. + :return: + """ + return self._neutral_temperature_f2d + + @property + def neutral_temperature_f3d(self): + """ + Array of neutral atom (effective) temperature at each mesh cell. + :return: + """ + return self._neutral_temperature_f3d + @neutral_temperature.setter def neutral_temperature(self, value): - num_neutrals = len([sp for sp in self.species_list if sp[1] == 0]) - _check_array("neutral_temperature", value, (num_neutrals, self.mesh.ny, self.mesh.nx)) - + _check_array("neutral_temperature", value, (len(self._neutral_list), self.mesh.ny, self.mesh.nx)) self._neutral_temperature = value + self._neutral_temperature_f2d = {} + self._neutral_temperature_f3d = {} + for k, sp in enumerate(self._neutral_list): + self._neutral_temperature_f2d[k] = SOLPSFunction2D.instance(self._inside_mesh, value[k]) + self._neutral_temperature_f2d[sp] = self._neutral_temperature_f2d[k] + self._neutral_temperature_f3d[k] = AxisymmetricMapper(self._neutral_temperature_f2d[k]) + self._neutral_temperature_f3d[sp] = self._neutral_temperature_f3d[k] @property def electron_density(self): @@ -141,11 +213,28 @@ def electron_density(self): """ return self._electron_density + @property + def electron_density_f2d(self): + """ + Simulated electron densities at each mesh cell. + :return: + """ + return self._electron_density_f2d + + @property + def electron_density_f3d(self): + """ + Simulated electron densities at each mesh cell. + :return: + """ + return self._electron_density_f3d + @electron_density.setter def electron_density(self, value): _check_array("electron_density", value, (self.mesh.ny, self.mesh.nx)) - self._electron_density = value + self._electron_density_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) + self._electron_density_f3d = AxisymmetricMapper(self._electron_density_f2d) @property def species_density(self): @@ -155,11 +244,33 @@ def species_density(self): """ return self._species_density + @property + def species_density_f2d(self): + """ + Array of species densities at each mesh cell. + :return: + """ + return self._species_density_f2d + + @property + def species_density_f3d(self): + """ + Array of species densities at each mesh cell. + :return: + """ + return self._species_density_f3d + @species_density.setter def species_density(self, value): - _check_array("species_density", value, (len(self.species_list), self.mesh.ny, self.mesh.nx)) - + _check_array("species_density", value, (len(self._species_list), self.mesh.ny, self.mesh.nx)) self._species_density = value + self._species_density_f2d = {} + self._species_density_f3d = {} + for k, sp in enumerate(self._species_list): + self._species_density_f2d[k] = SOLPSFunction2D.instance(self._inside_mesh, value[k]) + self._species_density_f2d[sp] = self._species_density_f2d[k] + self._species_density_f3d[k] = AxisymmetricMapper(self._species_density_f2d[k]) + self._species_density_f3d[sp] = self._species_density_f3d[k] @property def velocities(self): @@ -179,8 +290,15 @@ def velocities(self, value): for k in range(value.shape[0]): velocities_cartesian[k, :2] = self.mesh.to_cartesian(value[k, :2]) - self._velocities_cartesian = velocities_cartesian self._velocities = value + self._velocities_cartesian = velocities_cartesian + self._velocities_cartesian_f2d = {} + self._velocities_cartesian_f3d = {} + for k, sp in enumerate(self._species_list): + self._velocities_cartesian_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, velocities_cartesian[k]) + self._velocities_cartesian_f2d[sp] = self._velocities_cartesian_f2d[k] + self._velocities_cartesian_f3d[k] = VectorAxisymmetricMapper(self._velocities_cartesian_f2d[k]) + self._velocities_cartesian_f3d[sp] = self._velocities_cartesian_f3d[k] @property def velocities_cartesian(self): @@ -190,6 +308,22 @@ def velocities_cartesian(self): """ return self._velocities_cartesian + @property + def velocities_cartesian_f2d(self): + """ + Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. + :return: + """ + return self._velocities_cartesian_f2d + + @property + def velocities_cartesian_f3d(self): + """ + Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. + :return: + """ + return self._velocities_cartesian_f3d + @velocities_cartesian.setter def velocities_cartesian(self, value): _check_array("velocities_cartesian", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) @@ -200,18 +334,29 @@ def velocities_cartesian(self, value): for k in range(value.shape[0]): velocities[k, :2] = self.mesh.to_poloidal(value[k, :2]) - self._velocities = value self._velocities_cartesian = value + self._velocities = velocities + self._velocities_cartesian_f2d = {} + self._velocities_cartesian_f3d = {} + for k, sp in enumerate(self._species_list): + self._velocities_cartesian_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value[k]) + self._velocities_cartesian_f2d[sp] = self._velocities_cartesian_f2d[k] + self._velocities_cartesian_f3d[k] = VectorAxisymmetricMapper(self._velocities_cartesian_f2d[k]) + self._velocities_cartesian_f3d[sp] = self._velocities_cartesian_f3d[k] + + @property + def inside_mesh(self): + """ + Function2D for testing if point p is inside the simulation mesh. + """ + return self._inside_mesh @property def inside_volume_mesh(self): """ Function3D for testing if point p is inside the simulation mesh. """ - if self._inside_mesh is None: - raise RuntimeError("Inside mesh test not available for this simulation.") - else: - return self._inside_mesh + return AxisymmetricMapper(self._inside_mesh) @property def total_radiation(self): @@ -227,29 +372,40 @@ def total_radiation(self): else: return self._total_radiation - @total_radiation.setter - def total_radiation(self, value): - _check_array("total_radiation", value, (self.mesh.ny, self.mesh.nx)) + @property + def total_radiation_f2d(self): + """ + Total radiation - self._total_radiation = value + This is not calculated from the CHERAB emission models, instead it comes from the SOLPS output data. + Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' + and 'RQBRM'. Final output is in W/str? + """ + if self._total_radiation_f2d is None: + raise RuntimeError("Total radiation not available for this simulation.") + else: + return self._total_radiation_f2d - # TODO: decide is this a 2D or 3D interface? @property - def total_radiation_volume(self): + def total_radiation_f3d(self): """ - Total radiation volume. + Total radiation This is not calculated from the CHERAB emission models, instead it comes from the SOLPS output data. Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' and 'RQBRM'. Final output is in W/str? - - :returns: Function3D """ + if self._total_radiation_f3d is None: + raise RuntimeError("Total radiation not available for this simulation.") + else: + return self._total_radiation_f3d - mapped_radiation_data = _map_data_onto_triangles(self._total_rad) - radiation_mesh_2d = Discrete2DMesh(self.mesh.vertex_coordinates, self.mesh.triangles, mapped_radiation_data, limit=False) - # return AxisymmetricMapper(radiation_mesh_2d) - return radiation_mesh_2d + @total_radiation.setter + def total_radiation(self, value): + _check_array("total_radiation", value, (self.mesh.ny, self.mesh.nx)) + self._total_radiation = value + self._total_radiation_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) + self._total_radiation_f3d = AxisymmetricMapper(self._total_radiation_f2d) @property def b_field(self): @@ -272,6 +428,8 @@ def b_field(self, value): self._b_field_cartesian = b_field_cartesian self._b_field = value + self._b_field_cartesian_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, b_field_cartesian) + self._b_field_cartesian_f3d = VectorAxisymmetricMapper(self._b_field_cartesian_f2d) @property def b_field_cartesian(self): @@ -283,6 +441,26 @@ def b_field_cartesian(self): else: return self._b_field_cartesian + @property + def b_field_cartesian_f2d(self): + """ + Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). + """ + if self._b_field_cartesian_f2d is None: + raise RuntimeError("Magnetic field not available for this simulation.") + else: + return self._b_field_cartesian_f2d + + @property + def b_field_cartesian_f3d(self): + """ + Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). + """ + if self._b_field_cartesian_f3d is None: + raise RuntimeError("Magnetic field not available for this simulation.") + else: + return self._b_field_cartesian_f3d + @b_field_cartesian.setter def b_field_cartesian(self, value): _check_array("b_field_cartesian", value, (3, self.mesh.ny, self.mesh.nx)) @@ -294,6 +472,8 @@ def b_field_cartesian(self, value): self._b_field = b_field self._b_field_cartesian = value + self._b_field_cartesian_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value) + self._b_field_cartesian_f3d = VectorAxisymmetricMapper(self._b_field_cartesian_f2d) @property def eirene_simulation(self): @@ -317,17 +497,16 @@ def eirene_simulation(self, value): def __getstate__(self): state = { 'mesh': self._mesh.__getstate__(), + 'species_list': self._species_list, + 'inside_mesh': self._inside_mesh, + 'sample_vector_f2d': self._sample_vector_f2d, 'electron_temperature': self._electron_temperature, 'ion_temperature': self._ion_temperature, 'neutral_temperature': self._neutral_temperature, 'electron_density': self._electron_density, - 'species_list': self._species_list, 'species_density': self._species_density, - 'velocities': self._velocities, 'velocities_cartesian': self._velocities_cartesian, - 'inside_mesh': self._inside_mesh, 'total_radiation': self._total_radiation, - 'b_field': self._b_field, 'b_field_cartesian': self._b_field_cartesian, 'eirene_model': self._eirene_model, 'b2_model': self._b2_model, @@ -337,18 +516,52 @@ def __getstate__(self): def __setstate__(self, state): self._mesh = SOLPSMesh(**state['mesh']) - self._electron_temperature = state['electron_temperature'] - self._ion_temperature = state['ion_temperature'] - self._neutral_temperature = state['neutral_temperature'] - self._electron_density = state['electron_density'] self._species_list = state['species_list'] - self._species_density = state['species_density'] - self._velocities = state['velocities'] - self._velocities_cartesian = state['velocities_cartesian'] + self._neutral_list = tuple([sp for sp in self._species_list if sp[1] == 0]) self._inside_mesh = state['inside_mesh'] - self._total_radiation = state['total_radiation'] - self._b_field = state['b_field'] - self._b_field_cartesian = state['b_field_cartesian'] + self._sample_vector_f2d = state['sample_vector_f2d'] + self._electron_temperature = None + self._electron_temperature_f2d = None + self._electron_temperature_f3d = None + self._electron_density = None + self._electron_density_f2d = None + self._electron_density_f3d = None + self._ion_temperature = None + self._ion_temperature_f2d = None + self._ion_temperature_f3d = None + self._neutral_temperature = None + self._neutral_temperature_f2d = None + self._neutral_temperature_f3d = None + self._species_density = None + self._species_density_f2d = None + self._species_density_f3d = None + self._velocities = None + self._velocities_cartesian = None + self._velocities_cartesian_f2d = None + self._velocities_cartesian_f3d = None + self._total_radiation = None + self._total_radiation_f2d = None + self._total_radiation_f3d = None + self._b_field = None + self._b_field_cartesian = None + self._b_field_cartesian_f2d = None + self._b_field_cartesian_f3d = None + if state['electron_temperature'] is not None: + self.electron_temperature = state['electron_temperature'] # will create _f2d() and _f3d() + if state['ion_temperature'] is not None: + self.ion_temperature = state['ion_temperature'] + if state['neutral_temperature'] is not None: + self.neutral_temperature = state['neutral_temperature'] + if state['electron_density'] is not None: + self.electron_density = state['electron_density'] + if state['species_density'] is not None: + self.species_density = state['species_density'] + if state['velocities_cartesian'] is not None: + self.velocities_cartesian = state['velocities_cartesian'] + if state['total_radiation'] is not None: + self.total_radiation = state['total_radiation'] + if state['b_field_cartesian'] is not None: + self.b_field_cartesian = state['b_field_cartesian'] self._eirene_model = state['eirene_model'] self._b2_model = state['b2_model'] self._eirene = state['eirene'] @@ -512,31 +725,19 @@ def create_plasma(self, parent=None, transform=None, name=None): plasma.geometry = Cylinder(radius, height) plasma.geometry_transform = translate(0, 0, mesh.mesh_extent['minz']) - tri_index_lookup = self.mesh.triangle_index_lookup - tri_to_grid = self.mesh.triangle_to_grid_map - try: - plasma.b_field = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.b_field_cartesian) + plasma.b_field = self.b_field_cartesian_f3d except RuntimeError: print('Warning! No magnetic field data available for this simulation.') # Create electron species - triangle_data = _map_data_onto_triangles(self.electron_temperature) - electron_te_interp = Discrete2DMesh(mesh.vertex_coordinates, mesh.triangles, triangle_data, limit=False) - electron_temp = AxisymmetricMapper(electron_te_interp) - triangle_data = _map_data_onto_triangles(self.electron_density) - electron_dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) - plasma.electron_distribution = Maxwellian(electron_dens, electron_temp, electron_velocity, electron_mass) - - # Ion temperature - triangle_data = _map_data_onto_triangles(self.ion_temperature) - ion_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - - if self.velocities_cartesian is None: + plasma.electron_distribution = Maxwellian(self.electron_density_f3d, self.electron_temperature_f3d, electron_velocity, electron_mass) + + if self.velocities_cartesian_f3d is None: print('Warning! No velocity field data available for this simulation.') - if self.neutral_temperature is None: + if self.neutral_temperature_f3d is None: print('Warning! No neutral atom temperature data available for this simulation.') neutral_i = 0 # neutrals count @@ -549,24 +750,19 @@ def create_plasma(self, parent=None, transform=None, name=None): charge = sp[1] - triangle_data = _map_data_onto_triangles(self.species_density[k]) - dens = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - - # dens = SOLPSFunction3D(tri_index_lookup, tri_to_grid, self.species_density[k]) - # Create the velocity vector lookup function if self.velocities_cartesian is not None: - velocity = SOLPSVectorFunction3D(tri_index_lookup, tri_to_grid, self.velocities_cartesian[k]) + velocity = self.velocities_cartesian_f3d[k] else: velocity = lambda x, y, z: Vector3D(0, 0, 0) if charge or self.neutral_temperature is None: # ions or neutral atoms (neutral temperature is not available) - distribution = Maxwellian(dens, ion_temp, velocity, species_type.atomic_weight * atomic_mass) + distribution = Maxwellian(self.species_density_f3d[k], self.ion_temperature_f3d, velocity, + species_type.atomic_weight * atomic_mass) else: # neutral atoms with neutral temperature - triangle_data = _map_data_onto_triangles(self.neutral_temperature[neutral_i]) - neutral_temp = AxisymmetricMapper(Discrete2DMesh.instance(electron_te_interp, triangle_data)) - distribution = Maxwellian(dens, neutral_temp, velocity, species_type.atomic_weight * atomic_mass) + distribution = Maxwellian(self.species_density_f3d[k], self._neutral_temperature_f3d[neutral_i], velocity, + species_type.atomic_weight * atomic_mass) neutral_i += 1 plasma.composition.add(Species(species_type, charge, distribution)) @@ -574,22 +770,6 @@ def create_plasma(self, parent=None, transform=None, name=None): return plasma -def _map_data_onto_triangles(solps_dataset): - """ - Reshape a SOLPS data array so that it matches the triangles in the SOLPS mesh. - - :param ndarray solps_dataset: Given SOLPS dataset, typically of shape (98 x 32). - :return: New 1D ndarray with shape (98*32*2) - """ - - triangle_data = np.zeros(solps_dataset.size * 2, dtype=np.float64) - - triangle_data[::2] = solps_dataset.flatten() - triangle_data[1::2] = triangle_data[::2] - - return triangle_data - - def _check_array(name, value, shape): if not isinstance(value, np.ndarray): raise ValueError('Attribute "%s" must be a numpy.ndarray' % name) From 9b31601c9036e4dde37700e57fbf2559a440bf54 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Fri, 11 Sep 2020 03:02:24 +0300 Subject: [PATCH 18/25] Updated __getstate__, __setstate__ in SOLPSSimulation. --- cherab/solps/solps_plasma.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 6b36b55..1e86657 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -498,8 +498,6 @@ def __getstate__(self): state = { 'mesh': self._mesh.__getstate__(), 'species_list': self._species_list, - 'inside_mesh': self._inside_mesh, - 'sample_vector_f2d': self._sample_vector_f2d, 'electron_temperature': self._electron_temperature, 'ion_temperature': self._ion_temperature, 'neutral_temperature': self._neutral_temperature, @@ -515,37 +513,6 @@ def __getstate__(self): return state def __setstate__(self, state): - self._mesh = SOLPSMesh(**state['mesh']) - self._species_list = state['species_list'] - self._neutral_list = tuple([sp for sp in self._species_list if sp[1] == 0]) - self._inside_mesh = state['inside_mesh'] - self._sample_vector_f2d = state['sample_vector_f2d'] - self._electron_temperature = None - self._electron_temperature_f2d = None - self._electron_temperature_f3d = None - self._electron_density = None - self._electron_density_f2d = None - self._electron_density_f3d = None - self._ion_temperature = None - self._ion_temperature_f2d = None - self._ion_temperature_f3d = None - self._neutral_temperature = None - self._neutral_temperature_f2d = None - self._neutral_temperature_f3d = None - self._species_density = None - self._species_density_f2d = None - self._species_density_f3d = None - self._velocities = None - self._velocities_cartesian = None - self._velocities_cartesian_f2d = None - self._velocities_cartesian_f3d = None - self._total_radiation = None - self._total_radiation_f2d = None - self._total_radiation_f3d = None - self._b_field = None - self._b_field_cartesian = None - self._b_field_cartesian_f2d = None - self._b_field_cartesian_f3d = None if state['electron_temperature'] is not None: self.electron_temperature = state['electron_temperature'] # will create _f2d() and _f3d() if state['ion_temperature'] is not None: From 042812fea86697b5297caad00ecd57e77d2a57e1 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Fri, 11 Sep 2020 12:12:17 +0300 Subject: [PATCH 19/25] Properly distinguished poloidal (poloidal, radial, toroidal), cylindrical (R, phi, Z) and Cartesian (x, y, z) coordinates, e_tor = -e_phi. --- cherab/solps/formats/mdsplus.py | 12 +- cherab/solps/formats/raw_simulation_files.py | 12 +- cherab/solps/solps_plasma.py | 174 +++++++++---------- 3 files changed, 99 insertions(+), 99 deletions(-) diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index 8a62e18..0c1ae60 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -67,7 +67,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): ########################## # Magnetic field vectors # sim.b_field = conn.get('\SOLPS::TOP.SNAPSHOT.B').data()[:3] - # sim.b_field_cartesian is created authomatically + # sim.b_field_cylindrical is created authomatically # Load electron temperature and density sim.electron_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TE').data() @@ -94,7 +94,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): radial_flux = np.concatenate((np.zeros((ns, 1, nx)), radial_flux), axis=1) # Obtaining velocity from B2 flux - velocities_cartesian = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cartesian) + velocities_cylindrical = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cylindrical) # Obtaining additional data from EIRENE and replacing data for neutrals @@ -116,10 +116,10 @@ def load_solps_from_mdsplus(mds_server, ref_number): neutral_radial_flux = conn.get('\SOLPS::TOP.SNAPSHOT.RFLA').data()[:] if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): - neutral_velocities_cartesian = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, - parallel_velocity[neutral_indx], sim.b_field_cartesian) + neutral_velocities_cylindrical = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, + parallel_velocity[neutral_indx], sim.b_field_cylindrical) - velocities_cartesian[neutral_indx] = neutral_velocities_cartesian + velocities_cylindrical[neutral_indx] = neutral_velocities_cylindrical except (mdsExceptions.TreeNNF, TypeError): pass @@ -130,7 +130,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): pass sim.species_density = species_density - sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities + sim.velocities_cylindrical = velocities_cylindrical # this also updates sim.velocities ############################### # Load extra data from server # diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 487d578..9e56248 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -94,7 +94,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Load magnetic field sim.b_field = geom_data_dict['bb'][:3] - # sim.b_field_cartesian is created authomatically + # sim.b_field_cylindrical is created authomatically # Load electron species sim.electron_temperature = mesh_data_dict['te'] / elementary_charge @@ -114,7 +114,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): radial_flux = mesh_data_dict['fna'][1::2] # Obtaining velocity from B2 flux - velocities_cartesian = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cartesian) + velocities_cylindrical = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cylindrical) if not b2_standalone: # Obtaining additional data from EIRENE and replacing data for neutrals @@ -137,10 +137,10 @@ def load_solps_from_raw_output(simulation_path, debug=False): neutral_parallel_velocity = np.zeros((len(neutral_indx), ny, nx)) # must be zero outside EIRENE grid neutral_parallel_velocity[:, 1:-1, 1:-1] = parallel_velocity[neutral_indx, 1:-1, 1:-1] - neutral_velocities_cartesian = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, - neutral_parallel_velocity, sim.b_field_cartesian) + neutral_velocities_cylindrical = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, + neutral_parallel_velocity, sim.b_field_cylindrical) - velocities_cartesian[neutral_indx] = neutral_velocities_cartesian + velocities_cylindrical[neutral_indx] = neutral_velocities_cylindrical # Obtaining neutral temperatures ta = np.zeros((eirene.ta.shape[0], ny, nx)) @@ -162,7 +162,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.eirene_simulation = eirene sim.species_density = species_density - sim.velocities_cartesian = velocities_cartesian # this also updates sim.velocities + sim.velocities_cylindrical = velocities_cylindrical # this also updates sim.velocities return sim diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 1e86657..236c570 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -77,16 +77,16 @@ def __init__(self, mesh, species_list): self._species_density_f2d = None self._species_density_f3d = None self._velocities = None + self._velocities_cylindrical = None + self._velocities_cylindrical_f2d = None self._velocities_cartesian = None - self._velocities_cartesian_f2d = None - self._velocities_cartesian_f3d = None self._total_radiation = None self._total_radiation_f2d = None self._total_radiation_f3d = None self._b_field = None + self._b_field_cylindrical = None + self._b_field_cylindrical_f2d = None self._b_field_cartesian = None - self._b_field_cartesian_f2d = None - self._b_field_cartesian_f3d = None self._eirene_model = None # what is this for? self._b2_model = None # what is this for? self._eirene = None # do we need this in SOLPSSimulation? @@ -284,65 +284,65 @@ def velocities(self): def velocities(self, value): _check_array("velocities", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) - # Converting to Cartesian coordinates - velocities_cartesian = np.zeros(value.shape) - velocities_cartesian[:, 2] = value[:, 2] + # Converting to cylindrical coordinates + velocities_cylindrical = np.zeros(value.shape) + velocities_cylindrical[:, 1] = -value[:, 2] for k in range(value.shape[0]): - velocities_cartesian[k, :2] = self.mesh.to_cartesian(value[k, :2]) + velocities_cylindrical[k, (0, 2)] = self.mesh.to_cartesian(value[k, :2]) self._velocities = value - self._velocities_cartesian = velocities_cartesian - self._velocities_cartesian_f2d = {} - self._velocities_cartesian_f3d = {} + self._velocities_cylindrical = velocities_cylindrical + self._velocities_cylindrical_f2d = {} + self._velocities_cartesian = {} for k, sp in enumerate(self._species_list): - self._velocities_cartesian_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, velocities_cartesian[k]) - self._velocities_cartesian_f2d[sp] = self._velocities_cartesian_f2d[k] - self._velocities_cartesian_f3d[k] = VectorAxisymmetricMapper(self._velocities_cartesian_f2d[k]) - self._velocities_cartesian_f3d[sp] = self._velocities_cartesian_f3d[k] + self._velocities_cylindrical_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, velocities_cylindrical[k]) + self._velocities_cylindrical_f2d[sp] = self._velocities_cylindrical_f2d[k] + self._velocities_cartesian[k] = VectorAxisymmetricMapper(self._velocities_cylindrical_f2d[k]) + self._velocities_cartesian[sp] = self._velocities_cartesian[k] @property - def velocities_cartesian(self): + def velocities_cylindrical(self): """ - Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. + Velocities in Cartesian (v_r, v_phi, v_z) coordinates for each species densities at each mesh cell. :return: """ - return self._velocities_cartesian + return self._velocities_cylindrical @property - def velocities_cartesian_f2d(self): + def velocities_cylindrical_f2d(self): """ - Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. + Velocities in Cartesian (v_r, v_phi, v_z) coordinates for each species densities at each mesh cell. :return: """ - return self._velocities_cartesian_f2d + return self._velocities_cylindrical_f2d @property - def velocities_cartesian_f3d(self): + def velocities_cartesian(self): """ - Velocities in Cartesian (v_r, v_z, v_toroidal) coordinates for each species densities at each mesh cell. + Velocities in Cartesian (v_x, v_y, v_z) coordinates for each species densities at each mesh cell. :return: """ - return self._velocities_cartesian_f3d + return self._velocities_cartesian - @velocities_cartesian.setter - def velocities_cartesian(self, value): - _check_array("velocities_cartesian", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) + @velocities_cylindrical.setter + def velocities_cylindrical(self, value): + _check_array("velocities_cylindrical", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) # Converting to poloidal coordinates velocities = np.zeros(value.shape) - velocities[:, 2] = value[:, 2] + velocities[:, 2] = -value[:, 1] for k in range(value.shape[0]): - velocities[k, :2] = self.mesh.to_poloidal(value[k, :2]) + velocities[k, :2] = self.mesh.to_poloidal(value[k, (0, 2)]) - self._velocities_cartesian = value + self._velocities_cylindrical = value self._velocities = velocities - self._velocities_cartesian_f2d = {} - self._velocities_cartesian_f3d = {} + self._velocities_cylindrical_f2d = {} + self._velocities_cartesian = {} for k, sp in enumerate(self._species_list): - self._velocities_cartesian_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value[k]) - self._velocities_cartesian_f2d[sp] = self._velocities_cartesian_f2d[k] - self._velocities_cartesian_f3d[k] = VectorAxisymmetricMapper(self._velocities_cartesian_f2d[k]) - self._velocities_cartesian_f3d[sp] = self._velocities_cartesian_f3d[k] + self._velocities_cylindrical_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value[k]) + self._velocities_cylindrical_f2d[sp] = self._velocities_cylindrical_f2d[k] + self._velocities_cartesian[k] = VectorAxisymmetricMapper(self._velocities_cylindrical_f2d[k]) + self._velocities_cartesian[sp] = self._velocities_cartesian[k] @property def inside_mesh(self): @@ -422,58 +422,58 @@ def b_field(self, value): _check_array("b_field", value, (3, self.mesh.ny, self.mesh.nx)) # Converting to cartesian system - b_field_cartesian = np.zeros(value.shape) - b_field_cartesian[2] = value[2] - b_field_cartesian[:2] = self.mesh.to_cartesian(value[:2]) + b_field_cylindrical = np.zeros(value.shape) + b_field_cylindrical[1] = -value[2] + b_field_cylindrical[(0, 2), :] = self.mesh.to_cartesian(value[:2]) - self._b_field_cartesian = b_field_cartesian + self._b_field_cylindrical = b_field_cylindrical self._b_field = value - self._b_field_cartesian_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, b_field_cartesian) - self._b_field_cartesian_f3d = VectorAxisymmetricMapper(self._b_field_cartesian_f2d) + self._b_field_cylindrical_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, b_field_cylindrical) + self._b_field_cartesian = VectorAxisymmetricMapper(self._b_field_cylindrical_f2d) @property - def b_field_cartesian(self): + def b_field_cylindrical(self): """ - Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). + Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_phi, B_z). """ - if self._b_field_cartesian is None: + if self._b_field_cylindrical is None: raise RuntimeError("Magnetic field not available for this simulation.") else: - return self._b_field_cartesian + return self._b_field_cylindrical @property - def b_field_cartesian_f2d(self): + def b_field_cylindrical_f2d(self): """ - Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). + Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_phi, B_z). """ - if self._b_field_cartesian_f2d is None: + if self._b_field_cylindrical_f2d is None: raise RuntimeError("Magnetic field not available for this simulation.") else: - return self._b_field_cartesian_f2d + return self._b_field_cylindrical_f2d @property - def b_field_cartesian_f3d(self): + def b_field_cartesian(self): """ - Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_z, B_toroidal). + Magnetic B field at each mesh cell in Cartesian coordinates (B_x, B_y, B_z). """ - if self._b_field_cartesian_f3d is None: + if self._b_field_cartesian is None: raise RuntimeError("Magnetic field not available for this simulation.") else: - return self._b_field_cartesian_f3d + return self._b_field_cartesian - @b_field_cartesian.setter - def b_field_cartesian(self, value): - _check_array("b_field_cartesian", value, (3, self.mesh.ny, self.mesh.nx)) + @b_field_cylindrical.setter + def b_field_cylindrical(self, value): + _check_array("b_field_cylindrical", value, (3, self.mesh.ny, self.mesh.nx)) # Converting to poloidal system b_field = np.zeros(value.shape) - b_field[2] = value[2] - b_field[:2] = self.mesh.to_poloidal(value[:2]) + b_field[2] = -value[1] + b_field[:2] = self.mesh.to_poloidal(value[(0, 2), :]) self._b_field = b_field - self._b_field_cartesian = value - self._b_field_cartesian_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value) - self._b_field_cartesian_f3d = VectorAxisymmetricMapper(self._b_field_cartesian_f2d) + self._b_field_cylindrical = value + self._b_field_cylindrical_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value) + self._b_field_cartesian = VectorAxisymmetricMapper(self._b_field_cylindrical_f2d) @property def eirene_simulation(self): @@ -503,9 +503,9 @@ def __getstate__(self): 'neutral_temperature': self._neutral_temperature, 'electron_density': self._electron_density, 'species_density': self._species_density, - 'velocities_cartesian': self._velocities_cartesian, + 'velocities_cylindrical': self._velocities_cylindrical, 'total_radiation': self._total_radiation, - 'b_field_cartesian': self._b_field_cartesian, + 'b_field_cylindrical': self._b_field_cylindrical, 'eirene_model': self._eirene_model, 'b2_model': self._b2_model, 'eirene': self._eirene @@ -523,12 +523,12 @@ def __setstate__(self, state): self.electron_density = state['electron_density'] if state['species_density'] is not None: self.species_density = state['species_density'] - if state['velocities_cartesian'] is not None: - self.velocities_cartesian = state['velocities_cartesian'] + if state['velocities_cylindrical'] is not None: + self.velocities_cylindrical = state['velocities_cylindrical'] if state['total_radiation'] is not None: self.total_radiation = state['total_radiation'] - if state['b_field_cartesian'] is not None: - self.b_field_cartesian = state['b_field_cartesian'] + if state['b_field_cylindrical'] is not None: + self.b_field_cylindrical = state['b_field_cylindrical'] self._eirene_model = state['eirene_model'] self._b2_model = state['b2_model'] self._eirene = state['eirene'] @@ -693,7 +693,7 @@ def create_plasma(self, parent=None, transform=None, name=None): plasma.geometry_transform = translate(0, 0, mesh.mesh_extent['minz']) try: - plasma.b_field = self.b_field_cartesian_f3d + plasma.b_field = self.b_field_cartesian except RuntimeError: print('Warning! No magnetic field data available for this simulation.') @@ -701,7 +701,7 @@ def create_plasma(self, parent=None, transform=None, name=None): electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) plasma.electron_distribution = Maxwellian(self.electron_density_f3d, self.electron_temperature_f3d, electron_velocity, electron_mass) - if self.velocities_cartesian_f3d is None: + if self.velocities_cartesian is None: print('Warning! No velocity field data available for this simulation.') if self.neutral_temperature_f3d is None: @@ -719,7 +719,7 @@ def create_plasma(self, parent=None, transform=None, name=None): # Create the velocity vector lookup function if self.velocities_cartesian is not None: - velocity = self.velocities_cartesian_f3d[k] + velocity = self.velocities_cartesian[k] else: velocity = lambda x, y, z: Vector3D(0, 0, 0) @@ -755,7 +755,7 @@ def prefer_element(isotope): return isotope -def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cartesian): +def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cylindrical): """ Calculates velocities of neutral particles using B2 particle fluxes defined at cell faces. @@ -770,8 +770,8 @@ def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velo array of shape (num_atoms, mesh.ny, mesh.nx). Parallel velocity is a velocity projection on magnetic field direction. - :param ndarray b_field_cartesian: Magnetic field in Cartesian (R, Z, phi) coordinates. - Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). + :param ndarray b_field_cylindrical: Magnetic field in Cartesian (R, phi, Z) coordinates. + Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of shape (num_atoms, 3, mesh.ny, mesh.nx) @@ -785,7 +785,7 @@ def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velo _check_array('poloidal_flux', poloidal_flux, (ns, ny, nx)) _check_array('radial_flux', radial_flux, (ns, ny, nx)) _check_array('parallel_velocity', parallel_velocity, (ns, ny, nx)) - _check_array('b_field_cartesian', b_field_cartesian, (3, ny, nx)) + _check_array('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) poloidal_area = mesh.poloidal_area[None] radial_area = mesh.radial_area[None] @@ -830,17 +830,17 @@ def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velo vcart = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates # Projection of velocity on RZ-plane - vcart[:, :2] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) + vcart[:, (0, 2)] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) # Obtaining toroidal velocity - b = b_field_cartesian[None] + b = b_field_cylindrical[None] bmagn = np.sqrt((b * b).sum(1)) - vcart[:, 2] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 1] * b[:, 1]) / b[:, 2] + vcart[:, 1] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 2] * b[:, 2]) / b[:, 1] return vcart -def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cartesian): +def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cylindrical): """ Calculates velocities of neutral particles using Eirene particle fluxes defined at cell centre. @@ -855,10 +855,10 @@ def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_ array of shape (num_atoms, mesh.ny, mesh.nx). Parallel velocity is a velocity projection on magnetic field direction. - :param ndarray b_field_cartesian: Magnetic field in Cartesian (R, Z, phi) coordinates. - Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). + :param ndarray b_field_cylindrical: Magnetic field in Cartesian (R, phi, Z) coordinates. + Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). - :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of + :return: Velocities of atoms in (R, phi, Z) coordinates as a 4-dimensional ndarray of shape (mesh.nx, mesh.ny, num_atoms, 3) """ @@ -870,20 +870,20 @@ def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_ _check_array('poloidal_flux', poloidal_flux, (ns, ny, nx)) _check_array('radial_flux', radial_flux, (ns, ny, nx)) _check_array('parallel_velocity', parallel_velocity, (ns, ny, nx)) - _check_array('b_field_cartesian', b_field_cartesian, (3, ny, nx)) + _check_array('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) # Obtaining velocity poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) - vcart = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates + vcart = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates # Projection of velocity on RZ-plane - vcart[:, :2] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector[None] + radial_velocity[:, None] * mesh.radial_basis_vector[None]) + vcart[:, (0, 2)] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector[None] + radial_velocity[:, None] * mesh.radial_basis_vector[None]) # Obtaining toroidal velocity - b = b_field_cartesian[None] + b = b_field_cylindrical[None] bmagn = np.sqrt((b * b).sum(1)) - vcart[:, 2] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 1] * b[:, 1]) / b[:, 2] + vcart[:, 1] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 2] * b[:, 2]) / b[:, 1] return vcart From f153221ec8f9a4ed7dbe5f375d129bcc2e4a334c Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sat, 12 Sep 2020 02:33:58 +0300 Subject: [PATCH 20/25] Added docstrings to new SOLPSSimulation attributes and some safety checks --- cherab/solps/mesh_geometry.py | 11 -- cherab/solps/solps_2d_functions.pyx | 10 +- cherab/solps/solps_plasma.py | 175 ++++++++++++++++++---------- 3 files changed, 122 insertions(+), 74 deletions(-) diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index b4dd30f..4e82c9b 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -263,17 +263,6 @@ def triangle_to_grid_map(self): """ return self._triangle_to_grid_map - # @property - # def triangle_index_lookup(self): - # """ - # Discrete2DMesh instance that looks up a triangle index at any 2D point. - - # Useful for mapping from a 2D point -> triangle cell -> parent SOLPS mesh cell - - # :return: Discrete2DMesh instance - # """ - # return self._tri_index_loopup - def __getstate__(self): state = { 'r': self._r, diff --git a/cherab/solps/solps_2d_functions.pyx b/cherab/solps/solps_2d_functions.pyx index 6088878..bc52d74 100755 --- a/cherab/solps/solps_2d_functions.pyx +++ b/cherab/solps/solps_2d_functions.pyx @@ -42,6 +42,10 @@ cdef class SOLPSFunction2D(Function2D): # use numpy arrays to store data internally vertex_coords = np.array(vertex_coords, dtype=np.float64) triangles = np.array(triangles, dtype=np.int32) + triangle_to_grid_map = np.array(triangle_to_grid_map, dtype=np.int32) + + # Atention!!! Do not copy grid_data! Attribute self._grid_data must point to the original data array, + # so as not to re-initialize the interpolator if the user changes data values. # build kdtree self._kdtree = MeshKDTree2D(vertex_coords, triangles) @@ -128,6 +132,10 @@ cdef class SOLPSVectorFunction2D(VectorFunction2D): # use numpy arrays to store data internally vertex_coords = np.array(vertex_coords, dtype=np.float64) triangles = np.array(triangles, dtype=np.int32) + triangle_to_grid_map = np.array(triangle_to_grid_map, dtype=np.int32) + + # Atention!!! Do not copy grid_vectors! Attribute self._grid_vectors must point to the original data array, + # so as not to re-initialize the interpolator if the user changes data values. # build kdtree self._kdtree = MeshKDTree2D(vertex_coords, triangles) @@ -161,7 +169,7 @@ cdef class SOLPSVectorFunction2D(VectorFunction2D): repeated rebuilding of the mesh acceleration structures by sharing the geometry data between multiple interpolator objects. :param SOLPSVectorFunction2D instance: SOLPSVectorFunction2D object. - :param ndarray grid_data: An array containing data on SOLPS grid. + :param ndarray grid_vectors: An array containing vector data on SOLPS grid. :return: A SOLPSVectorFunction2D object. :rtype: SOLPSVectorFunction2D """ diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 236c570..e2124ca 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -111,6 +111,7 @@ def species_list(self): def electron_temperature(self): """ Simulated electron temperatures at each mesh cell. + Array of shape (ny, nx). :return: """ return self._electron_temperature @@ -118,7 +119,8 @@ def electron_temperature(self): @property def electron_temperature_f2d(self): """ - Simulated electron temperatures at each mesh cell. + Function2D interpolator for electron temperature. + Returns electron temperature at a given point (R, Z). :return: """ return self._electron_temperature_f2d @@ -126,14 +128,16 @@ def electron_temperature_f2d(self): @property def electron_temperature_f3d(self): """ - Simulated electron temperatures at each mesh cell. + Function3D interpolator for electron temperature. + Returns electron temperature at a given point (x, y, z). :return: """ return self._electron_temperature_f3d @electron_temperature.setter def electron_temperature(self, value): - _check_array("electron_temperature", value, (self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("electron_temperature", value, (self.mesh.ny, self.mesh.nx)) self._electron_temperature = value self._electron_temperature_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) self._electron_temperature_f3d = AxisymmetricMapper(self._electron_temperature_f2d) @@ -142,6 +146,7 @@ def electron_temperature(self, value): def ion_temperature(self): """ Simulated ion temperatures at each mesh cell. + Array of shape (ny, nx). :return: """ return self._ion_temperature @@ -149,7 +154,8 @@ def ion_temperature(self): @property def ion_temperature_f2d(self): """ - Simulated ion temperatures at each mesh cell. + Function2D interpolator for ion temperature. + Returns ion temperature at a given point (R, Z). :return: """ return self._ion_temperature_f2d @@ -157,14 +163,16 @@ def ion_temperature_f2d(self): @property def ion_temperature_f3d(self): """ - Simulated ion temperatures at each mesh cell. + Function3D interpolator for ion temperature. + Returns ion temperature at a given point (x, y, z). :return: """ return self._ion_temperature_f3d @ion_temperature.setter def ion_temperature(self, value): - _check_array("ion_temperature", value, (self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("ion_temperature", value, (self.mesh.ny, self.mesh.nx)) self._ion_temperature = value self._ion_temperature_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) self._ion_temperature_f3d = AxisymmetricMapper(self._ion_temperature_f2d) @@ -172,7 +180,8 @@ def ion_temperature(self, value): @property def neutral_temperature(self): """ - Array of neutral atom (effective) temperature at each mesh cell. + Simulated neutral atom (effective) temperatures at each mesh cell. + Array of shape (na, ny, nx). :return: """ return self._neutral_temperature @@ -180,7 +189,10 @@ def neutral_temperature(self): @property def neutral_temperature_f2d(self): """ - Array of neutral atom (effective) temperature at each mesh cell. + Dictionary of Function2D interpolators for neutral atom (effective) temperatures. + Accessed by neutral atom index or neutral_list elements. + E.g., neutral_temperature_f2d[0] or neutral_temperature_f2d[('deuterium', 0))]. + Each entry returns neutral atom (effective) temperature at a given point (R, Z). :return: """ return self._neutral_temperature_f2d @@ -188,14 +200,18 @@ def neutral_temperature_f2d(self): @property def neutral_temperature_f3d(self): """ - Array of neutral atom (effective) temperature at each mesh cell. + Dictionary of Function3D interpolators for neutral atom (effective) temperatures. + Accessed by neutral atom index or neutral_list elements. + E.g., neutral_temperature_f3d[0] or neutral_temperature_f3d[('deuterium', 0))]. + Each entry returns neutral atom (effective) temperature at a given point (x, y, z). :return: """ return self._neutral_temperature_f3d @neutral_temperature.setter def neutral_temperature(self, value): - _check_array("neutral_temperature", value, (len(self._neutral_list), self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("neutral_temperature", value, (len(self._neutral_list), self.mesh.ny, self.mesh.nx)) self._neutral_temperature = value self._neutral_temperature_f2d = {} self._neutral_temperature_f3d = {} @@ -209,6 +225,7 @@ def neutral_temperature(self, value): def electron_density(self): """ Simulated electron densities at each mesh cell. + Array of shape (ny, nx) :return: """ return self._electron_density @@ -216,7 +233,8 @@ def electron_density(self): @property def electron_density_f2d(self): """ - Simulated electron densities at each mesh cell. + Function2D interpolator for electron density. + Returns electron density at a given point (R, Z). :return: """ return self._electron_density_f2d @@ -224,14 +242,16 @@ def electron_density_f2d(self): @property def electron_density_f3d(self): """ - Simulated electron densities at each mesh cell. + Function3D interpolator for electron density. + Returns electron density at a given point (x, y, z). :return: """ return self._electron_density_f3d @electron_density.setter def electron_density(self, value): - _check_array("electron_density", value, (self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("electron_density", value, (self.mesh.ny, self.mesh.nx)) self._electron_density = value self._electron_density_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) self._electron_density_f3d = AxisymmetricMapper(self._electron_density_f2d) @@ -239,7 +259,8 @@ def electron_density(self, value): @property def species_density(self): """ - Array of species densities at each mesh cell. + Simulated species densities at each mesh cell. + Array of shape (ns, ny, nx). :return: """ return self._species_density @@ -247,7 +268,10 @@ def species_density(self): @property def species_density_f2d(self): """ - Array of species densities at each mesh cell. + Dictionary of Function2D interpolators for species densities. + Accessed by species index or species_list elements. + E.g., species_density_f2d[1] or species_density_f2d[('deuterium', 1))]. + Each entry returns species density at a given point (R, Z). :return: """ return self._species_density_f2d @@ -255,14 +279,18 @@ def species_density_f2d(self): @property def species_density_f3d(self): """ - Array of species densities at each mesh cell. + Dictionary of Function3D interpolators for species densities. + Accessed by species index or species_list elements. + E.g., species_density_f3d[1] or species_density_f3d[('deuterium', 1))]. + Each entry returns species density at a given point (x, y, z). :return: """ return self._species_density_f3d @species_density.setter def species_density(self, value): - _check_array("species_density", value, (len(self._species_list), self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("species_density", value, (len(self._species_list), self.mesh.ny, self.mesh.nx)) self._species_density = value self._species_density_f2d = {} self._species_density_f3d = {} @@ -275,14 +303,17 @@ def species_density(self, value): @property def velocities(self): """ - Velocities in poloidal coordinates (v_poloidal, v_radial, v_toroidal) for each species densities at each mesh cell. + Species velocities in poloidal coordinates (e_pol, e_rad, e_tor) at each mesh cell. + Array of shape (ns, 3, ny, nx): + [:, 0, :, :] - poloidal, [:, 1, :, :] - radial, [:, 2, :, :] - toroidal. :return: """ return self._velocities @velocities.setter def velocities(self, value): - _check_array("velocities", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("velocities", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) # Converting to cylindrical coordinates velocities_cylindrical = np.zeros(value.shape) @@ -303,7 +334,8 @@ def velocities(self, value): @property def velocities_cylindrical(self): """ - Velocities in Cartesian (v_r, v_phi, v_z) coordinates for each species densities at each mesh cell. + Species velocities in cylindrical coordinates (R, phi, Z) at each mesh cell. + Array of shape (ns, 3, ny, nx): [:, 0, :, :] - R, [:, 1, :, :] - phi, [:, 2, :, :] - Z. :return: """ return self._velocities_cylindrical @@ -311,7 +343,11 @@ def velocities_cylindrical(self): @property def velocities_cylindrical_f2d(self): """ - Velocities in Cartesian (v_r, v_phi, v_z) coordinates for each species densities at each mesh cell. + Dictionary of VectorFunction2D interpolators for species velocities + in cylindrical coordinates. + Accessed by species index or species_list elements. + E.g., elocities_cylindrical_f2d[1] or elocities_cylindrical_f2d[('deuterium', 1))]. + Each entry returns a vector of species velocity at a given point (R, Z). :return: """ return self._velocities_cylindrical_f2d @@ -319,14 +355,19 @@ def velocities_cylindrical_f2d(self): @property def velocities_cartesian(self): """ - Velocities in Cartesian (v_x, v_y, v_z) coordinates for each species densities at each mesh cell. + Dictionary of VectorFunction3D interpolators for species velocities + in cartesian coordinates. + Accessed by species index or species_list elements. + E.g., elocities_cylindrical_f3d[1] or elocities_cylindrical_f3d[('deuterium', 1))]. + Each entry returns a vector of species velocity at a given point (x, y, z). :return: """ return self._velocities_cartesian @velocities_cylindrical.setter def velocities_cylindrical(self, value): - _check_array("velocities_cylindrical", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("velocities_cylindrical", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) # Converting to poloidal coordinates velocities = np.zeros(value.shape) @@ -361,7 +402,8 @@ def inside_volume_mesh(self): @property def total_radiation(self): """ - Total radiation + Total radiation at each mesh cell. + Array of shape (ny, nx). This is not calculated from the CHERAB emission models, instead it comes from the SOLPS output data. Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' @@ -375,11 +417,8 @@ def total_radiation(self): @property def total_radiation_f2d(self): """ - Total radiation - - This is not calculated from the CHERAB emission models, instead it comes from the SOLPS output data. - Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' - and 'RQBRM'. Final output is in W/str? + Function2D interpolator for total radiation. + Returns total radiation at a given point (R, Z). """ if self._total_radiation_f2d is None: raise RuntimeError("Total radiation not available for this simulation.") @@ -389,11 +428,8 @@ def total_radiation_f2d(self): @property def total_radiation_f3d(self): """ - Total radiation - - This is not calculated from the CHERAB emission models, instead it comes from the SOLPS output data. - Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' - and 'RQBRM'. Final output is in W/str? + Function3D interpolator for total radiation. + Returns total radiation at a given point (x, y, z). """ if self._total_radiation_f3d is None: raise RuntimeError("Total radiation not available for this simulation.") @@ -402,7 +438,8 @@ def total_radiation_f3d(self): @total_radiation.setter def total_radiation(self, value): - _check_array("total_radiation", value, (self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("total_radiation", value, (self.mesh.ny, self.mesh.nx)) self._total_radiation = value self._total_radiation_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) self._total_radiation_f3d = AxisymmetricMapper(self._total_radiation_f2d) @@ -410,7 +447,8 @@ def total_radiation(self, value): @property def b_field(self): """ - Magnetic B field at each mesh cell in mesh cell coordinates (b_poloidal, b_radial, b_toroidal). + Magnetic B field in poloidal coordinates (e_pol, e_rad, e_tor) at each mesh cell. + Array of shape (3, ny, nx): [0, :, :] - poloidal, [1, :, :] - radial, [2, :, :] - toroidal. """ if self._b_field is None: raise RuntimeError("Magnetic field not available for this simulation.") @@ -419,7 +457,8 @@ def b_field(self): @b_field.setter def b_field(self, value): - _check_array("b_field", value, (3, self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("b_field", value, (3, self.mesh.ny, self.mesh.nx)) # Converting to cartesian system b_field_cylindrical = np.zeros(value.shape) @@ -434,7 +473,8 @@ def b_field(self, value): @property def b_field_cylindrical(self): """ - Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_phi, B_z). + Magnetic B field in poloidal coordinates (R, phi, Z) at each mesh cell. + Array of shape (3, ny, nx): [0, :, :] - R, [1, :, :] - phi, [2, :, :] - Z. """ if self._b_field_cylindrical is None: raise RuntimeError("Magnetic field not available for this simulation.") @@ -444,7 +484,8 @@ def b_field_cylindrical(self): @property def b_field_cylindrical_f2d(self): """ - Magnetic B field at each mesh cell in Cartesian coordinates (B_r, B_phi, B_z). + VectorFunction2D interpolator for magnetic B field in cylindrical coordinates. + Returns a vector of magnetic field at a given point (R, Z). """ if self._b_field_cylindrical_f2d is None: raise RuntimeError("Magnetic field not available for this simulation.") @@ -454,7 +495,8 @@ def b_field_cylindrical_f2d(self): @property def b_field_cartesian(self): """ - Magnetic B field at each mesh cell in Cartesian coordinates (B_x, B_y, B_z). + VectorFunction2D interpolator for magnetic B field in Cartesian coordinates. + Returns a vector of magnetic field at a given point (x, y, z). """ if self._b_field_cartesian is None: raise RuntimeError("Magnetic field not available for this simulation.") @@ -463,7 +505,8 @@ def b_field_cartesian(self): @b_field_cylindrical.setter def b_field_cylindrical(self, value): - _check_array("b_field_cylindrical", value, (3, self.mesh.ny, self.mesh.nx)) + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("b_field_cylindrical", value, (3, self.mesh.ny, self.mesh.nx)) # Converting to poloidal system b_field = np.zeros(value.shape) @@ -737,9 +780,7 @@ def create_plasma(self, parent=None, transform=None, name=None): return plasma -def _check_array(name, value, shape): - if not isinstance(value, np.ndarray): - raise ValueError('Attribute "%s" must be a numpy.ndarray' % name) +def _check_shape(name, value, shape): if value.shape != shape: raise ValueError('Shape of "%s": %s mismatch the shape of SOLPS grid: %s.' % (name, value.shape, shape)) @@ -776,16 +817,21 @@ def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velo :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of shape (num_atoms, 3, mesh.ny, mesh.nx) """ + density = np.array(density, dtype=np.float64, copy=False) + poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) + radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) + parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) + b_field_cylindrical = np.array(b_field_cylindrical, dtype=np.float64, copy=False) nx = mesh.nx # poloidal ny = mesh.ny # radial ns = density.shape[0] # number of species - _check_array('density', density, (ns, ny, nx)) - _check_array('poloidal_flux', poloidal_flux, (ns, ny, nx)) - _check_array('radial_flux', radial_flux, (ns, ny, nx)) - _check_array('parallel_velocity', parallel_velocity, (ns, ny, nx)) - _check_array('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) + _check_shape('density', density, (ns, ny, nx)) + _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_shape('radial_flux', radial_flux, (ns, ny, nx)) + _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) + _check_shape('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) poloidal_area = mesh.poloidal_area[None] radial_area = mesh.radial_area[None] @@ -827,17 +873,17 @@ def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velo velocity_top = velocity_bottom[:, :, topiy, topix] velocity_top[:, :, (topix < 0) + (topiy < 0)] = 0 - vcart = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates + vcyl = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates # Projection of velocity on RZ-plane - vcart[:, (0, 2)] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) + vcyl[:, (0, 2)] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) # Obtaining toroidal velocity b = b_field_cylindrical[None] bmagn = np.sqrt((b * b).sum(1)) - vcart[:, 1] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 2] * b[:, 2]) / b[:, 1] + vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[:, 0] - vcyl[:, 2] * b[:, 2]) / b[:, 1] - return vcart + return vcyl def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cylindrical): @@ -861,29 +907,34 @@ def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_ :return: Velocities of atoms in (R, phi, Z) coordinates as a 4-dimensional ndarray of shape (mesh.nx, mesh.ny, num_atoms, 3) """ + density = np.array(density, dtype=np.float64, copy=False) + poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) + radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) + parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) + b_field_cylindrical = np.array(b_field_cylindrical, dtype=np.float64, copy=False) nx = mesh.nx # poloidal ny = mesh.ny # radial ns = density.shape[0] # number of neutral atoms - _check_array('density', density, (ns, ny, nx)) - _check_array('poloidal_flux', poloidal_flux, (ns, ny, nx)) - _check_array('radial_flux', radial_flux, (ns, ny, nx)) - _check_array('parallel_velocity', parallel_velocity, (ns, ny, nx)) - _check_array('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) + _check_shape('density', density, (ns, ny, nx)) + _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_shape('radial_flux', radial_flux, (ns, ny, nx)) + _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) + _check_shape('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) # Obtaining velocity poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) - vcart = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates + vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates # Projection of velocity on RZ-plane - vcart[:, (0, 2)] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector[None] + radial_velocity[:, None] * mesh.radial_basis_vector[None]) + vcyl[:, (0, 2)] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector[None] + radial_velocity[:, None] * mesh.radial_basis_vector[None]) # Obtaining toroidal velocity b = b_field_cylindrical[None] bmagn = np.sqrt((b * b).sum(1)) - vcart[:, 1] = (parallel_velocity * bmagn - vcart[:, 0] * b[:, 0] - vcart[:, 2] * b[:, 2]) / b[:, 1] + vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[:, 0] - vcyl[:, 2] * b[:, 2]) / b[:, 1] - return vcart + return vcyl From a7b890ae00c508f09a5d7af1894c1153ea63d3af Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sat, 12 Sep 2020 02:44:55 +0300 Subject: [PATCH 21/25] Fixed a typo --- cherab/solps/solps_2d_functions.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cherab/solps/solps_2d_functions.pyx b/cherab/solps/solps_2d_functions.pyx index bc52d74..1e1aa0f 100755 --- a/cherab/solps/solps_2d_functions.pyx +++ b/cherab/solps/solps_2d_functions.pyx @@ -44,7 +44,7 @@ cdef class SOLPSFunction2D(Function2D): triangles = np.array(triangles, dtype=np.int32) triangle_to_grid_map = np.array(triangle_to_grid_map, dtype=np.int32) - # Atention!!! Do not copy grid_data! Attribute self._grid_data must point to the original data array, + # Attention!!! Do not copy grid_data! Attribute self._grid_data must point to the original data array, # so as not to re-initialize the interpolator if the user changes data values. # build kdtree @@ -134,7 +134,7 @@ cdef class SOLPSVectorFunction2D(VectorFunction2D): triangles = np.array(triangles, dtype=np.int32) triangle_to_grid_map = np.array(triangle_to_grid_map, dtype=np.int32) - # Atention!!! Do not copy grid_vectors! Attribute self._grid_vectors must point to the original data array, + # Attention!!! Do not copy grid_vectors! Attribute self._grid_vectors must point to the original data array, # so as not to re-initialize the interpolator if the user changes data values. # build kdtree From b9819de396e8382b344ad77d679f7c728629e620 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sat, 12 Sep 2020 22:24:01 +0300 Subject: [PATCH 22/25] Moved b2_flux_to_velocity() and eirene_flux_to_velocity() to SOLPSSimulation, fixed a bug in flux density calculation. Added electron_velocities_* attributes. --- cherab/solps/formats/mdsplus.py | 21 +- cherab/solps/formats/raw_simulation_files.py | 16 +- cherab/solps/solps_plasma.py | 426 +++++++++++-------- 3 files changed, 266 insertions(+), 197 deletions(-) diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index 0c1ae60..5a00144 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -22,7 +22,7 @@ from cherab.core.atomic.elements import lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, b2_flux_to_velocity, eirene_flux_to_velocity +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element from matplotlib import pyplot as plt @@ -77,7 +77,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): sim.ion_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TI').data() # Load species density - species_density = conn.get('\SOLPS::TOP.SNAPSHOT.NA').data() + sim.species_density = conn.get('\SOLPS::TOP.SNAPSHOT.NA').data() # Load parallel velocity parallel_velocity = conn.get('\SOLPS::TOP.SNAPSHOT.UA').data() @@ -93,16 +93,17 @@ def load_solps_from_mdsplus(mds_server, ref_number): if radial_flux.shape[1] == ny - 1: radial_flux = np.concatenate((np.zeros((ns, 1, nx)), radial_flux), axis=1) - # Obtaining velocity from B2 flux - velocities_cylindrical = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cylindrical) + # Setting velocities from B2 flux + sim.b2_flux_to_velocity(poloidal_flux, radial_flux, parallel_velocity) # Obtaining additional data from EIRENE and replacing data for neutrals b2_standalone = False try: # Replace the species densities - neutral_density = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() - species_density[neutral_indx] = neutral_density[:] # this will throw a TypeError is neutral_density is not an array + neutral_density = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() # this will throw a TypeError is neutral_density is not an array + # We can update the data without re-initialising interpolators because they use pointers + sim.species_density[neutral_indx] = neutral_density[:] except (mdsExceptions.TreeNNF, TypeError): print("Warning! This is B2 stand-alone simulation.") @@ -116,10 +117,7 @@ def load_solps_from_mdsplus(mds_server, ref_number): neutral_radial_flux = conn.get('\SOLPS::TOP.SNAPSHOT.RFLA').data()[:] if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): - neutral_velocities_cylindrical = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, - parallel_velocity[neutral_indx], sim.b_field_cylindrical) - - velocities_cylindrical[neutral_indx] = neutral_velocities_cylindrical + sim.eirene_flux_to_velocity(neutral_poloidal_flux, neutral_radial_flux, parallel_velocity[neutral_indx]) except (mdsExceptions.TreeNNF, TypeError): pass @@ -129,9 +127,6 @@ def load_solps_from_mdsplus(mds_server, ref_number): except (mdsExceptions.TreeNNF, TypeError): pass - sim.species_density = species_density - sim.velocities_cylindrical = velocities_cylindrical # this also updates sim.velocities - ############################### # Load extra data from server # ############################### diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 9e56248..895d6b8 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -28,7 +28,7 @@ from cherab.solps.eirene import load_fort44_file from cherab.solps.b2.parse_b2_block_file import load_b2f_file from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, b2_flux_to_velocity, eirene_flux_to_velocity +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element # Code based on script by Felix Reimold (2016) @@ -104,7 +104,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.ion_temperature = mesh_data_dict['ti'] / elementary_charge # Load species density - species_density = mesh_data_dict['na'] + sim.species_density = mesh_data_dict['na'] # Load parallel velocity parallel_velocity = mesh_data_dict['ua'] @@ -114,7 +114,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): radial_flux = mesh_data_dict['fna'][1::2] # Obtaining velocity from B2 flux - velocities_cylindrical = b2_flux_to_velocity(mesh, species_density, poloidal_flux, radial_flux, parallel_velocity, sim.b_field_cylindrical) + sim.b2_flux_to_velocity(poloidal_flux, radial_flux, parallel_velocity) if not b2_standalone: # Obtaining additional data from EIRENE and replacing data for neutrals @@ -123,7 +123,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): neutral_density = np.zeros((len(neutral_indx), ny, nx)) neutral_density[:, 1:-1, 1:-1] = eirene.da - species_density[neutral_indx] = neutral_density + sim.species_density[neutral_indx] = neutral_density # Obtaining neutral atom velocity from EIRENE flux # Note that if the output for fluxes was turned off, eirene.ppa and eirene.rpa are all zeros @@ -137,10 +137,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): neutral_parallel_velocity = np.zeros((len(neutral_indx), ny, nx)) # must be zero outside EIRENE grid neutral_parallel_velocity[:, 1:-1, 1:-1] = parallel_velocity[neutral_indx, 1:-1, 1:-1] - neutral_velocities_cylindrical = eirene_flux_to_velocity(mesh, neutral_density, neutral_poloidal_flux, neutral_radial_flux, - neutral_parallel_velocity, sim.b_field_cylindrical) - - velocities_cylindrical[neutral_indx] = neutral_velocities_cylindrical + sim.eirene_flux_to_velocity(neutral_poloidal_flux, neutral_radial_flux, neutral_parallel_velocity) # Obtaining neutral temperatures ta = np.zeros((eirene.ta.shape[0], ny, nx)) @@ -161,9 +158,6 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.eirene_simulation = eirene - sim.species_density = species_density - sim.velocities_cylindrical = velocities_cylindrical # this also updates sim.velocities - return sim def create_mesh_from_geom_data(geom_data): diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index e2124ca..759402e 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -20,7 +20,7 @@ import pickle import numpy as np import matplotlib.pyplot as plt -from scipy.constants import atomic_mass, electron_mass +from scipy.constants import atomic_mass, electron_mass, elementary_charge # Raysect imports from raysect.core import translate, Point3D, Vector3D, Node, AffineMatrix3D @@ -67,6 +67,10 @@ def __init__(self, mesh, species_list): self._electron_density = None self._electron_density_f2d = None self._electron_density_f3d = None + self._electron_velocities = None + self._electron_velocities_cylindrical = None + self._electron_velocities_cylindrical_f2d = None + self._electron_velocities_cartesian = None self._ion_temperature = None self._ion_temperature_f2d = None self._ion_temperature_f3d = None @@ -256,6 +260,73 @@ def electron_density(self, value): self._electron_density_f2d = SOLPSFunction2D.instance(self._inside_mesh, value) self._electron_density_f3d = AxisymmetricMapper(self._electron_density_f2d) + @property + def electron_velocities(self): + """ + Electron velocities in poloidal coordinates (e_pol, e_rad, e_tor) at each mesh cell. + Array of shape (3, ny, nx): + [0, :, :] - poloidal, [1, :, :] - radial, [2, :, :] - toroidal. + :return: + """ + return self._electron_velocities + + @electron_velocities.setter + def electron_velocities(self, value): + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("electron_velocities", value, (3, self.mesh.ny, self.mesh.nx)) + + # Converting to cylindrical coordinates + velocities_cylindrical = np.zeros(value.shape) + velocities_cylindrical[1] = -value[2] + velocities_cylindrical[[0, 2]] = self.mesh.to_cartesian(value[:2]) + + self._electron_velocities = value + self._electron_velocities_cylindrical = velocities_cylindrical + self._electron_velocities_cylindrical_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, velocities_cylindrical) + self._electron_velocities_cartesian = VectorAxisymmetricMapper(self._electron_velocities_cylindrical_f2d) + + @property + def electron_velocities_cylindrical(self): + """ + Electron velocities in cylindrical coordinates (R, phi, Z) at each mesh cell. + Array of shape (3, ny, nx): [0, :, :] - R, [1, :, :] - phi, [2, :, :] - Z. + :return: + """ + return self._electron_velocities_cylindrical + + @electron_velocities_cylindrical.setter + def electron_velocities_cylindrical(self, value): + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("electron_velocities_cylindrical", value, (3, self.mesh.ny, self.mesh.nx)) + + # Converting to poloidal coordinates + velocities = np.zeros(value.shape) + velocities[:, 2] = -value[:, 1] + velocities[:2] = self.mesh.to_poloidal(value[[0, 2]]) + + self._electron_velocities_cylindrical = value + self._electron_velocities = velocities + self._electron_velocities_cylindrical_f2d = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value) + self._electron_velocities_cartesian = VectorAxisymmetricMapper(self._electron_velocities_cylindrical_f2d) + + @property + def electron_velocities_cylindrical_f2d(self): + """ + VectorFunction2D interpolator for electron velocities in cylindrical coordinates. + Returns a vector of electron velocity at a given point (R, Z). + :return: + """ + return self._electron_velocities_cylindrical_f2d + + @property + def electron_velocities_cartesian(self): + """ + VectorFunction3D interpolator for electron velocities in Cartesian coordinates. + Returns a vector of electron velocity at a given point (x, y, z). + :return: + """ + return self._electron_velocities_cartesian + @property def species_density(self): """ @@ -319,7 +390,7 @@ def velocities(self, value): velocities_cylindrical = np.zeros(value.shape) velocities_cylindrical[:, 1] = -value[:, 2] for k in range(value.shape[0]): - velocities_cylindrical[k, (0, 2)] = self.mesh.to_cartesian(value[k, :2]) + velocities_cylindrical[k, [0, 2]] = self.mesh.to_cartesian(value[k, :2]) self._velocities = value self._velocities_cylindrical = velocities_cylindrical @@ -340,6 +411,27 @@ def velocities_cylindrical(self): """ return self._velocities_cylindrical + @velocities_cylindrical.setter + def velocities_cylindrical(self, value): + value = np.array(value, dtype=np.float64, copy=False) + _check_shape("velocities_cylindrical", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) + + # Converting to poloidal coordinates + velocities = np.zeros(value.shape) + velocities[:, 2] = -value[:, 1] + for k in range(value.shape[0]): + velocities[k, :2] = self.mesh.to_poloidal(value[k, [0, 2]]) + + self._velocities_cylindrical = value + self._velocities = velocities + self._velocities_cylindrical_f2d = {} + self._velocities_cartesian = {} + for k, sp in enumerate(self._species_list): + self._velocities_cylindrical_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value[k]) + self._velocities_cylindrical_f2d[sp] = self._velocities_cylindrical_f2d[k] + self._velocities_cartesian[k] = VectorAxisymmetricMapper(self._velocities_cylindrical_f2d[k]) + self._velocities_cartesian[sp] = self._velocities_cartesian[k] + @property def velocities_cylindrical_f2d(self): """ @@ -356,7 +448,7 @@ def velocities_cylindrical_f2d(self): def velocities_cartesian(self): """ Dictionary of VectorFunction3D interpolators for species velocities - in cartesian coordinates. + in Cartesian coordinates. Accessed by species index or species_list elements. E.g., elocities_cylindrical_f3d[1] or elocities_cylindrical_f3d[('deuterium', 1))]. Each entry returns a vector of species velocity at a given point (x, y, z). @@ -364,27 +456,6 @@ def velocities_cartesian(self): """ return self._velocities_cartesian - @velocities_cylindrical.setter - def velocities_cylindrical(self, value): - value = np.array(value, dtype=np.float64, copy=False) - _check_shape("velocities_cylindrical", value, (len(self.species_list), 3, self.mesh.ny, self.mesh.nx)) - - # Converting to poloidal coordinates - velocities = np.zeros(value.shape) - velocities[:, 2] = -value[:, 1] - for k in range(value.shape[0]): - velocities[k, :2] = self.mesh.to_poloidal(value[k, (0, 2)]) - - self._velocities_cylindrical = value - self._velocities = velocities - self._velocities_cylindrical_f2d = {} - self._velocities_cartesian = {} - for k, sp in enumerate(self._species_list): - self._velocities_cylindrical_f2d[k] = SOLPSVectorFunction2D.instance(self._sample_vector_f2d, value[k]) - self._velocities_cylindrical_f2d[sp] = self._velocities_cylindrical_f2d[k] - self._velocities_cartesian[k] = VectorAxisymmetricMapper(self._velocities_cylindrical_f2d[k]) - self._velocities_cartesian[sp] = self._velocities_cartesian[k] - @property def inside_mesh(self): """ @@ -460,10 +531,10 @@ def b_field(self, value): value = np.array(value, dtype=np.float64, copy=False) _check_shape("b_field", value, (3, self.mesh.ny, self.mesh.nx)) - # Converting to cartesian system + # Converting to cylindrical system b_field_cylindrical = np.zeros(value.shape) b_field_cylindrical[1] = -value[2] - b_field_cylindrical[(0, 2), :] = self.mesh.to_cartesian(value[:2]) + b_field_cylindrical[[0, 2]] = self.mesh.to_cartesian(value[:2]) self._b_field_cylindrical = b_field_cylindrical self._b_field = value @@ -511,7 +582,7 @@ def b_field_cylindrical(self, value): # Converting to poloidal system b_field = np.zeros(value.shape) b_field[2] = -value[1] - b_field[:2] = self.mesh.to_poloidal(value[(0, 2), :]) + b_field[:2] = self.mesh.to_poloidal(value[[0, 2]]) self._b_field = b_field self._b_field_cylindrical = value @@ -537,6 +608,152 @@ def eirene_simulation(self, value): self._eirene = value + def b2_flux_to_velocity(self, poloidal_flux, radial_flux, parallel_velocity): + """ + Sets velocities of all species using B2 particle fluxes defined at cell faces. + + :param ndarray poloidal_flux: Poloidal flux of atoms in s-1. Must be a 3 dimensional array of + shape (num_atoms, mesh.ny, mesh.nx). + :param ndarray radial_flux: Radial flux of atoms in s-1. Must be a 3 dimensional array of + shape (num_atoms, mesh.ny, mesh.nx). + :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional + array of shape (num_atoms, mesh.ny, mesh.nx). + Parallel velocity is a velocity projection on magnetic + field direction. + """ + if self.b_field_cylindrical is None: + raise RuntimeError('Attribute "b_field_cylindrical" is not set.') + if self.species_density is None: + raise RuntimeError('Attribute "species_density" is not set.') + + mesh = self.mesh + b = self.b_field_cylindrical + + poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) + radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) + parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) + + nx = mesh.nx # poloidal + ny = mesh.ny # radial + ns = len(self.species_list) # number of species + + _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_shape('radial_flux', radial_flux, (ns, ny, nx)) + _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) + + poloidal_area = mesh.poloidal_area + radial_area = mesh.radial_area + leftix = mesh.neighbix[0] # poloidal prev. + leftiy = mesh.neighbiy[0] + bottomix = mesh.neighbix[1] # radial prev. + bottomiy = mesh.neighbiy[1] + rightix = mesh.neighbix[2] # poloidal next. + rightiy = mesh.neighbiy[2] + topix = mesh.neighbix[3] # radial next. + topiy = mesh.neighbiy[3] + + # Converting s-1 --> m-2 s-1 + poloidal_flux = np.divide(poloidal_flux, poloidal_area, out=np.zeros_like(poloidal_flux), where=poloidal_area > 0) + radial_flux = np.divide(radial_flux, radial_area, out=np.zeros_like(radial_flux), where=radial_area > 0) + + # Obtaining left velocity + dens_neighb = self.species_density[:, leftiy, leftix] # density in the left neighbouring cell + has_neighbour = ((leftix > -1) * (leftiy > -1)) # check if has left neighbour + neg_flux = (poloidal_flux < 0) * (self.species_density > 0) # will use density in this cell if flux is negative + pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour # will use density in neighbouring cell if flux is positive + velocity_left = np.divide(poloidal_flux, self.species_density, out=np.zeros((ns, ny, nx)), where=neg_flux) + velocity_left = np.divide(poloidal_flux, dens_neighb, out=velocity_left, where=pos_flux) + poloidal_face_normal = mesh.radial_basis_vector[[1, 0]] + poloidal_face_normal[1] *= -1 + velocity_left = velocity_left[:, None] * poloidal_face_normal # to vector in RZ + + # Obtaining bottom velocity + dens_neighb = self.species_density[:, bottomiy, bottomix] + has_neighbour = ((bottomix > -1) * (bottomiy > -1)) + neg_flux = (radial_flux < 0) * (self.species_density > 0) + pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour + velocity_bottom = np.divide(radial_flux, self.species_density, out=np.zeros((ns, ny, nx)), where=neg_flux) + velocity_bottom = np.divide(radial_flux, dens_neighb, out=velocity_bottom, where=pos_flux) + radial_face_normal = mesh.poloidal_basis_vector[[1, 0]] + radial_face_normal[0] *= -1 + velocity_bottom = velocity_bottom[:, None] * radial_face_normal # to RZ + + # Obtaining right and top velocities + velocity_right = velocity_left[:, :, rightiy, rightix] + velocity_right[:, :, (rightix < 0) + (rightiy < 0)] = 0 + + velocity_top = velocity_bottom[:, :, topiy, topix] + velocity_top[:, :, (topix < 0) + (topiy < 0)] = 0 + + vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates + + # Projection of velocity on RZ-plane + vcyl[:, [0, 2]] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) + + # Obtaining toroidal velocity + bmagn = np.sqrt((b * b).sum(0)) + vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[0] - vcyl[:, 2] * b[2]) / b[1] + + self.velocities_cylindrical = vcyl + + def eirene_flux_to_velocity(self, poloidal_flux, radial_flux, parallel_velocity): + """ + Sets velocities of neutral atoms using Eirene particle fluxes defined at cell centre. + + :param ndarray poloidal_flux: Poloidal flux of atoms in m-2 s-1. Must be a 3 dimensional array of + shape (num_atoms, mesh.ny, mesh.nx). + :param ndarray radial_flux: Radial flux of atoms in m-2 s-1. Must be a 3 dimensional array of + shape (num_atoms, mesh.ny, mesh.nx). + :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional + array of shape (num_atoms, mesh.ny, mesh.nx). + Parallel velocity is a velocity projection on magnetic + field direction. + """ + if self.b_field_cylindrical is None: + raise RuntimeError('Attribute "b_field_cylindrical" is not set.') + if self.species_density is None: + raise RuntimeError('Attribute "species_density" is not set.') + + mesh = self.mesh + b = self.b_field_cylindrical + + poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) + radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) + parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) + + nx = mesh.nx # poloidal + ny = mesh.ny # radial + ns = len(self._neutral_list) # number of neutral atoms + + _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_shape('radial_flux', radial_flux, (ns, ny, nx)) + _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) + + neutral_indx = [k for k, sp in enumerate(self.species_list) if sp[1] == 0] + density = self.species_density[neutral_indx, :] + + # Obtaining velocity + poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) + radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) + + vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates + + # Projection of velocity on RZ-plane + vcyl[:, [0, 2]] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector + radial_velocity[:, None] * mesh.radial_basis_vector) + + # Obtaining toroidal velocity + bmagn = np.sqrt((b * b).sum(0)) + vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[0] - vcyl[:, 2] * b[2]) / b[1] + + if self.velocities_cylindrical is not None: # we need to update self._velocities also + vcyl_all = self.velocities_cylindrical + else: + vcyl_all = np.zeros(len(self.species_list, 3, ny, nx)) + + vcyl_all[neutral_indx] = vcyl + + self.velocities_cylindrical = vcyl_all + def __getstate__(self): state = { 'mesh': self._mesh.__getstate__(), @@ -546,6 +763,7 @@ def __getstate__(self): 'neutral_temperature': self._neutral_temperature, 'electron_density': self._electron_density, 'species_density': self._species_density, + 'electron_velocities_cylindrical': self._electron_velocities_cylindrical, 'velocities_cylindrical': self._velocities_cylindrical, 'total_radiation': self._total_radiation, 'b_field_cylindrical': self._b_field_cylindrical, @@ -566,6 +784,8 @@ def __setstate__(self, state): self.electron_density = state['electron_density'] if state['species_density'] is not None: self.species_density = state['species_density'] + if state['electron_velocities_cylindrical'] is not None: + self.velocities_cylindrical = state['electron_velocities_cylindrical'] if state['velocities_cylindrical'] is not None: self.velocities_cylindrical = state['velocities_cylindrical'] if state['total_radiation'] is not None: @@ -741,11 +961,15 @@ def create_plasma(self, parent=None, transform=None, name=None): print('Warning! No magnetic field data available for this simulation.') # Create electron species - electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) + if self.electron_velocities_cartesian is None: + print('Warning! No electron velocity data available for this simulation.') + electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) + else: + electron_velocity = self.electron_velocities_cartesian plasma.electron_distribution = Maxwellian(self.electron_density_f3d, self.electron_temperature_f3d, electron_velocity, electron_mass) if self.velocities_cartesian is None: - print('Warning! No velocity field data available for this simulation.') + print('Warning! No species velocities data available for this simulation.') if self.neutral_temperature_f3d is None: print('Warning! No neutral atom temperature data available for this simulation.') @@ -794,147 +1018,3 @@ def prefer_element(isotope): return isotope.element return isotope - - -def b2_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cylindrical): - """ - Calculates velocities of neutral particles using B2 particle fluxes defined at cell faces. - - :param SOLPSMesh mesh: SOLPS simulation mesh. - :param ndarray density: Density of atoms in m-3. Must be 3 dimensiona array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray poloidal_flux: Poloidal flux of atoms in s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray radial_flux: Radial flux of atoms in s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional - array of shape (num_atoms, mesh.ny, mesh.nx). - Parallel velocity is a velocity projection on magnetic - field direction. - :param ndarray b_field_cylindrical: Magnetic field in Cartesian (R, phi, Z) coordinates. - Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). - - :return: Velocities of atoms in (R, Z, phi) coordinates as a 4-dimensional ndarray of - shape (num_atoms, 3, mesh.ny, mesh.nx) - """ - density = np.array(density, dtype=np.float64, copy=False) - poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) - radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) - parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) - b_field_cylindrical = np.array(b_field_cylindrical, dtype=np.float64, copy=False) - - nx = mesh.nx # poloidal - ny = mesh.ny # radial - ns = density.shape[0] # number of species - - _check_shape('density', density, (ns, ny, nx)) - _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) - _check_shape('radial_flux', radial_flux, (ns, ny, nx)) - _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) - _check_shape('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) - - poloidal_area = mesh.poloidal_area[None] - radial_area = mesh.radial_area[None] - leftix = mesh.neighbix[0] # poloidal prev. - leftiy = mesh.neighbiy[0] - bottomix = mesh.neighbix[1] # radial prev. - bottomiy = mesh.neighbiy[1] - rightix = mesh.neighbix[2] # poloidal next. - rightiy = mesh.neighbiy[2] - topix = mesh.neighbix[3] # radial next. - topiy = mesh.neighbiy[3] - - # Converting s-1 --> m-2 s-1 - poloidal_flux = np.divide(poloidal_flux, poloidal_area, out=np.zeros_like(poloidal_flux), where=poloidal_area > 0) - radial_flux = np.divide(radial_flux, radial_area, out=np.zeros_like(radial_flux), where=radial_area > 0) - - # Obtaining left velocity - dens_neighb = density[:, leftiy, leftix] # density in the left neighbouring cell - has_neighbour = ((leftix > -1) * (leftiy > -1))[None] # check if has left neighbour - neg_flux = (poloidal_flux < 0) * (density > 0) # will use density in this cell if flux is negative - pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour # will use density in neighbouring cell if flux is positive - velocity_left = np.divide(poloidal_flux, density, out=np.zeros((ns, ny, nx)), where=neg_flux) - velocity_left = np.divide(poloidal_flux, dens_neighb, out=velocity_left, where=pos_flux) - velocity_left = velocity_left[:, None] * mesh.poloidal_basis_vector[None] # to vector in Cartesian - - # Obtaining bottom velocity - dens_neighb = density[:, bottomiy, bottomix] - has_neighbour = ((bottomix > -1) * (bottomiy > -1))[None] - neg_flux = (radial_flux < 0) * (density > 0) - pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour - velocity_bottom = np.divide(radial_flux, density, out=np.zeros((ns, ny, nx)), where=neg_flux) - velocity_bottom = np.divide(radial_flux, dens_neighb, out=velocity_bottom, where=pos_flux) - velocity_bottom = velocity_bottom[:, None] * mesh.radial_basis_vector[None] # to Cartesian - - # Obtaining right and top velocities - velocity_right = velocity_left[:, :, rightiy, rightix] - velocity_right[:, :, (rightix < 0) + (rightiy < 0)] = 0 - - velocity_top = velocity_bottom[:, :, topiy, topix] - velocity_top[:, :, (topix < 0) + (topiy < 0)] = 0 - - vcyl = np.zeros((ns, 3, ny, nx)) # velocities in Cartesian coordinates - - # Projection of velocity on RZ-plane - vcyl[:, (0, 2)] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) - - # Obtaining toroidal velocity - b = b_field_cylindrical[None] - bmagn = np.sqrt((b * b).sum(1)) - vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[:, 0] - vcyl[:, 2] * b[:, 2]) / b[:, 1] - - return vcyl - - -def eirene_flux_to_velocity(mesh, density, poloidal_flux, radial_flux, parallel_velocity, b_field_cylindrical): - """ - Calculates velocities of neutral particles using Eirene particle fluxes defined at cell centre. - - :param SOLPSMesh mesh: SOLPS simulation mesh. - :param ndarray density: Density of atoms in m-3. Must be 3 dimensiona array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray poloidal_flux: Poloidal flux of atoms in m-2 s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray radial_flux: Radial flux of atoms in m-2 s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional - array of shape (num_atoms, mesh.ny, mesh.nx). - Parallel velocity is a velocity projection on magnetic - field direction. - :param ndarray b_field_cylindrical: Magnetic field in Cartesian (R, phi, Z) coordinates. - Must be a 3 dimensional array of shape (3, mesh.ny, mesh.nx). - - :return: Velocities of atoms in (R, phi, Z) coordinates as a 4-dimensional ndarray of - shape (mesh.nx, mesh.ny, num_atoms, 3) - """ - density = np.array(density, dtype=np.float64, copy=False) - poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) - radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) - parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) - b_field_cylindrical = np.array(b_field_cylindrical, dtype=np.float64, copy=False) - - nx = mesh.nx # poloidal - ny = mesh.ny # radial - ns = density.shape[0] # number of neutral atoms - - _check_shape('density', density, (ns, ny, nx)) - _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) - _check_shape('radial_flux', radial_flux, (ns, ny, nx)) - _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) - _check_shape('b_field_cylindrical', b_field_cylindrical, (3, ny, nx)) - - # Obtaining velocity - poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) - radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) - - vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates - - # Projection of velocity on RZ-plane - vcyl[:, (0, 2)] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector[None] + radial_velocity[:, None] * mesh.radial_basis_vector[None]) - - # Obtaining toroidal velocity - b = b_field_cylindrical[None] - bmagn = np.sqrt((b * b).sum(1)) - vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[:, 0] - vcyl[:, 2] * b[:, 2]) / b[:, 1] - - return vcyl From 1a0b412568c4d939005ea11c760909b02b56a913 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 16 Sep 2020 02:10:51 +0300 Subject: [PATCH 23/25] eirene.elosm and eirene.edism now resolved over molecules like in fort.44. Made fort.44 version check strict again. --- cherab/solps/eirene/eirene.py | 4 ++-- cherab/solps/eirene/parser/fort44.py | 20 ++++++++------------ cherab/solps/eirene/parser/fort44_2013.py | 5 +++-- cherab/solps/eirene/parser/fort44_2017.py | 3 ++- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cherab/solps/eirene/eirene.py b/cherab/solps/eirene/eirene.py index 425bf5c..f4a5da6 100755 --- a/cherab/solps/eirene/eirene.py +++ b/cherab/solps/eirene/eirene.py @@ -395,7 +395,7 @@ def elosm(self): @elosm.setter def elosm(self, value): - self._check_dimensions(value, 1) + self._check_dimensions(value, self._nm) self._elosm = value @property @@ -409,7 +409,7 @@ def edism(self): @edism.setter def edism(self, value): - self._check_dimensions(value, 1) + self._check_dimensions(value, self._nm) self._edism = value @property diff --git a/cherab/solps/eirene/parser/fort44.py b/cherab/solps/eirene/parser/fort44.py index 06ba344..ce36d60 100644 --- a/cherab/solps/eirene/parser/fort44.py +++ b/cherab/solps/eirene/parser/fort44.py @@ -57,17 +57,13 @@ def assign_fort44_parser(file_version): :param file_version: Fort44 file version from the file header. :return: Parsing function object """ - fort44_supported_versions = ( - 20081111, - 20130210, - 20170328, - ) - if file_version not in fort44_supported_versions: - print("Warning! Version {} of fort.44 file has not been tested with this parser.".format(file_version)) + fort44_parser_library = { + 20170328: load_fort44_2017, + 20130210: load_fort44_2013 + } - if file_version >= 20170328: - - return load_fort44_2017 - - return load_fort44_2013 + if file_version in fort44_parser_library.keys(): + return fort44_parser_library[file_version] + else: + raise ValueError("Can't read version {} fort.44 file".format(file_version)) diff --git a/cherab/solps/eirene/parser/fort44_2013.py b/cherab/solps/eirene/parser/fort44_2013.py index 13ade5d..9795f8b 100644 --- a/cherab/solps/eirene/parser/fort44_2013.py +++ b/cherab/solps/eirene/parser/fort44_2013.py @@ -16,6 +16,7 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. +import numpy as np from cherab.solps.eirene import Eirene from cherab.solps.eirene.parser.utility import read_block44 @@ -98,8 +99,8 @@ def load_fort44_2013(file_path, debug=False): eirene.emism = read_block44(file_handle, 1, nx, ny) # Molecular Halpha Emission # Radiated power (elosm, edism, eradt) - eirene.elosm = read_block44(file_handle, 1, nx, ny) # Power loss due to molecules (including dissociation) - eirene.edism = read_block44(file_handle, 1, nx, ny) # Power loss due to molecule dissociation + eirene.elosm = read_block44(file_handle, nm, nx, ny) # Power loss due to molecules (including dissociation) + eirene.edism = read_block44(file_handle, nm, nx, ny) # Power loss due to molecule dissociation eirene.eradt = read_block44(file_handle, 1, nx, ny) # Neutral radiated power return eirene diff --git a/cherab/solps/eirene/parser/fort44_2017.py b/cherab/solps/eirene/parser/fort44_2017.py index b261c19..c3fbbdd 100644 --- a/cherab/solps/eirene/parser/fort44_2017.py +++ b/cherab/solps/eirene/parser/fort44_2017.py @@ -16,6 +16,7 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. +import numpy as np from cherab.solps.eirene import Eirene from cherab.solps.eirene.parser.utility import read_block44 @@ -105,7 +106,7 @@ def load_fort44_2017(file_path, debug=False): _ = read_block44(file_handle, eirene.nm, eirene.nx, eirene.ny) # Molecule particle source # Radiated power (elosm, edism, eradt) - eirene.edism = read_block44(file_handle, 1, eirene.nx, eirene.ny) # Power loss due to molecule dissociation + eirene.edism = read_block44(file_handle, eirene.nm, eirene.nx, eirene.ny) # Power loss due to molecule dissociation # Consume lines until eradt is reached while True: From 1c2a22f1e1e7cb336ff310cee30f273cf8467233 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Wed, 16 Sep 2020 02:13:57 +0300 Subject: [PATCH 24/25] Updated balance.py to match the functionality of mdsplus.py. --- cherab/solps/formats/balance.py | 246 +++++++++++-------- cherab/solps/formats/mdsplus.py | 5 +- cherab/solps/formats/raw_simulation_files.py | 9 +- 3 files changed, 149 insertions(+), 111 deletions(-) diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index a77a714..e40c930 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -38,98 +38,133 @@ def load_solps_from_balance(balance_filename): rydberg_energy = constants.value('Rydberg constant times hc in eV') # Open the file - fhandle = netcdf.netcdf_file(balance_filename, 'r') - - # Load SOLPS mesh geometry - mesh = load_mesh_from_netcdf(fhandle) - - # Load each plasma species in simulation - - species_list = [] - n_species = len(fhandle.variables['am'].data) - for i in range(n_species): - - # Extract the nuclear charge - if fhandle.variables['species'].data[i, 1] == b'D': - zn = 1 - if fhandle.variables['species'].data[i, 1] == b'C': - zn = 6 - if fhandle.variables['species'].data[i, 1] == b'N': - zn = 7 - if fhandle.variables['species'].data[i, 1] == b'N' and fhandle.variables['species'].data[i, 2] == b'e': - zn = 10 - if fhandle.variables['species'].data[i, 1] == b'A' and fhandle.variables['species'].data[i, 2] == b'r': - zn = 18 - - am = int(round(float(fhandle.variables['am'].data[i]))) # Atomic mass - charge = int(fhandle.variables['za'].data[i]) # Ionisation/charge - isotope = lookup_isotope(zn, number=am) - species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same - - # If we only need to populate species_list, there is probably a faster way to do this... - species_list.append((species.name, charge)) - - sim = SOLPSSimulation(mesh, species_list) - - # TODO: add code to load SOLPS velocities and magnetic field from files - - # Load electron species - sim.electron_temperature = fhandle.variables['te'].data.copy().T / el_charge - sim.electron_density = fhandle.variables['ne'].data.copy().T - - # Load ion temperature - sim.ion_temperature = fhandle.variables['ti'].data.copy().T / el_charge - - species_density = np.transpose(fhandle.variables['na'].data, (0, 2, 1)) - - # Load the neutrals data - try: - D0_indx = sim.species_list.index(("deuterium", 0)) - except ValueError: - D0_indx = None - - # Replace the deuterium neutrals density (from the fluid neutrals model by default) with - # the values calculated by EIRENE - do the same for other neutrals? - if 'dab2' in fhandle.variables.keys(): - if D0_indx is not None: - b2_len = np.shape(species_density[D0_indx])[-2] - dab2 = fhandle.variables['dab2'].data.copy()[0].T - eirene_len = np.shape(fhandle.variables['dab2'].data)[-2] - species_density[D0_indx] = dab2[0:b2_len - eirene_len, :] - - eirene_run = True - else: - eirene_run = False - - sim.species_density = species_density - - # Calculate the total radiated power - if eirene_run: - # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0).T / mesh.vol - - # Electron energy loss due to interactions with neutrals - if 'eirene_mc_eael_she_bal' in fhandle.variables.keys(): - eirene_ecoolrate = np.sum(fhandle.variables['eirene_mc_eael_she_bal'].data, axis=0).T / mesh.vol - - # Ionisation rate from EIRENE, needed to calculate the energy loss to overcome the ionisation potential of atoms - if 'eirene_mc_papl_sna_bal' in fhandle.variables.keys(): - tmp = np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1].T - eirene_potential_loss = rydberg_energy * tmp * el_charge / mesh.vol - - # This will be negative (energy sink); multiply by -1 - sim.total_radiation = -1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss)) - - else: - # Total radiated power from B2, not including neutrals - b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0).T / mesh.vol - - potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0).T / mesh.vol - - # Save total radiated power to the simulation object - sim.total_radiation = rydberg_energy * el_charge * potential_loss - b2_ploss - - fhandle.close() + with netcdf.netcdf_file(balance_filename, 'r') as fhandle: + + # Load SOLPS mesh geometry + mesh = load_mesh_from_netcdf(fhandle) + + # Load each plasma species in simulation + + species_list = [] + neutral_indx = [] + am = np.round(fhandle.variables['am'].data).astype(np.int) # Atomic mass number + charge = fhandle.variables['za'].data.astype(np.int) # Ionisation/charge + species_names = fhandle.variables['species'].data.copy() + ns = am.size + for i in range(ns): + symbol = ''.join([b.decode('utf-8').strip(' 0123456789+-') for b in species_names[i]]) # also strips isotope number + if symbol != 'D' and symbol != 'T': + isotope = lookup_isotope(symbol, number=am[i]) # will through an error for D or T + species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same + else: + species = lookup_isotope(symbol) + + # If we only need to populate species_list, there is probably a faster way to do this... + species_list.append((species.name, charge[i])) + if charge[i] == 0: + neutral_indx.append(i) + + sim = SOLPSSimulation(mesh, species_list) + nx = mesh.nx + ny = mesh.ny + + ########################## + # Magnetic field vectors # + sim.b_field = fhandle.variables['bb'].data.copy()[:3] + # sim.b_field_cylindrical is created authomatically + + # Load electron species + sim.electron_temperature = fhandle.variables['te'].data.copy() / el_charge + sim.electron_density = fhandle.variables['ne'].data.copy() + + # Load ion temperature + sim.ion_temperature = fhandle.variables['ti'].data.copy() / el_charge + + # Load species density + sim.species_density = fhandle.variables['na'].data.copy() + + # Load parallel velocity + parallel_velocity = fhandle.variables['ua'].data.copy() + + # Load poloidal and radial particle fluxes for velocity calculation + if 'fna' in fhandle.variables: + fna = fhandle.variables['fna'].data.copy() + poloidal_flux = fna[:, 0] + radial_flux = fna[:, 1] + elif 'fnax' in fhandle.variables and 'fnay' in fhandle.variables: + poloidal_flux = fhandle.variables['fnax'].data.copy() + radial_flux = fhandle.variables['fnay'].data.copy() + else: # trying to obtain particle flux from components + fna = 0 + for key in fhandle.variables.keys(): + if 'fna_' in key: + fna += fhandle.variables[key].data.copy() + if isinstance(fna, np.ndarray): + poloidal_flux = fna[:, 0] + radial_flux = fna[:, 1] + + # Obtaining velocity from B2 flux + sim.b2_flux_to_velocity(poloidal_flux, radial_flux, parallel_velocity) + + # Obtaining additional data from EIRENE and replacing data for neutrals + if 'dab2' in fhandle.variables: + sim.species_density[neutral_indx] = fhandle.variables['dab2'].data.copy()[:, :ny, :nx] # in case of large grid size + b2_standalone = False + else: + b2_standalone = True + + if not b2_standalone: + + # Obtaining neutral atom velocity from EIRENE flux + # Note that if the output for fluxes was turned off, pfluxa and rfluxa' are all zeros + if 'pfluxa' in fhandle.variables and 'rfluxa' in fhandle.variables: + neutral_poloidal_flux = fhandle.variables['pfluxa'].data.copy()[:, :ny, :nx] + neutral_radial_flux = fhandle.variables['rfluxa'].data.copy()[:, :ny, :nx] + + if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): + sim.eirene_flux_to_velocity(neutral_poloidal_flux, neutral_radial_flux, parallel_velocity[neutral_indx]) + + # Obtaining neutral temperatures + if 'tab2' in fhandle.variables: + sim.neutral_temperature = fhandle.variables['tab2'].data.copy()[:, :ny, :nx] + + # Calculate the total radiated power + b2_ploss = 0 + eirene_ecoolrate = 0 + eirene_potential_loss = 0 + + # Total radiated power from B2, not including neutrals + if 'b2stel_she_bal' in fhandle.variables: + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / mesh.vol + + # Electron energy loss due to interactions with neutrals + if 'eirene_mc_eael_she_bal' in fhandle.variables: + eirene_ecoolrate = np.sum(fhandle.variables['eirene_mc_eael_she_bal'].data, axis=0) / mesh.vol + + # Ionisation rate from EIRENE, needed to calculate the energy loss to overcome the ionisation potential of atoms + if 'eirene_mc_papl_sna_bal' in fhandle.variables: + tmp = np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1] + eirene_potential_loss = rydberg_energy * tmp * el_charge / mesh.vol + + # This will be negative (energy sink); multiply by -1 + total_rad = -1.0 * (b2_ploss + (eirene_ecoolrate - eirene_potential_loss)) + + else: + # Total radiated power from B2, not including neutrals + b2_ploss = 0 + potential_loss = 0 + + if 'b2stel_she_bal' in fhandle.variables: + b2_ploss = np.sum(fhandle.variables['b2stel_she_bal'].data, axis=0) / mesh.vol + + if 'b2stel_sna_ion_bal' in fhandle.variables: + potential_loss = np.sum(fhandle.variables['b2stel_sna_ion_bal'].data, axis=0) / mesh.vol + + # Save total radiated power to the simulation object + total_rad = rydberg_energy * el_charge * potential_loss - b2_ploss + + if isinstance(total_rad, np.ndarray): + sim.total_radiation = total_rad return sim @@ -138,28 +173,27 @@ def load_mesh_from_netcdf(fhandle): # Load SOLPS mesh geometry # Re-arrange the array dimensions in the way CHERAB expects... - r = np.transpose(fhandle.variables['crx'].data, (0, 2, 1)) - z = np.transpose(fhandle.variables['cry'].data, (0, 2, 1)) - vol = fhandle.variables['vol'].data.copy().T + r = fhandle.variables['crx'].data.copy() + z = fhandle.variables['cry'].data.copy() + vol = fhandle.variables['vol'].data.copy() # Loading neighbouring cell indices neighbix = np.zeros(r.shape, dtype=np.int) neighbiy = np.zeros(r.shape, dtype=np.int) - neighbix[0] = fhandle.variables['leftix'].data.copy().astype(np.int).T # poloidal prev. - neighbix[1] = fhandle.variables['bottomix'].data.copy().astype(np.int).T # radial prev. - neighbix[2] = fhandle.variables['rightix'].data.copy().astype(np.int).T # poloidal next - neighbix[3] = fhandle.variables['topix'].data.copy().astype(np.int).T # radial next + neighbix[0] = fhandle.variables['leftix'].data.astype(np.int) # poloidal prev. + neighbix[1] = fhandle.variables['bottomix'].data.astype(np.int) # radial prev. + neighbix[2] = fhandle.variables['rightix'].data.astype(np.int) # poloidal next + neighbix[3] = fhandle.variables['topix'].data.astype(np.int) # radial next - neighbiy[0] = fhandle.variables['leftiy'].data.copy().astype(np.int).T - neighbiy[1] = fhandle.variables['bottomiy'].data.copy().astype(np.int).T - neighbiy[2] = fhandle.variables['rightiy'].data.copy().astype(np.int).T - neighbiy[3] = fhandle.variables['topiy'].data.copy().astype(np.int).T + neighbiy[0] = fhandle.variables['leftiy'].data.astype(np.int) + neighbiy[1] = fhandle.variables['bottomiy'].data.astype(np.int) + neighbiy[2] = fhandle.variables['rightiy'].data.astype(np.int) + neighbiy[3] = fhandle.variables['topiy'].data.astype(np.int) # In SOLPS cell indexing starts with -1 (guarding cell), but in SOLPSMesh -1 means no neighbour. - if neighbix.min() < -1 or neighbiy.min() < -1: - neighbix += 1 - neighbiy += 1 + neighbix += 1 + neighbiy += 1 neighbix[neighbix == r.shape[2]] = -1 neighbiy[neighbiy == r.shape[1]] = -1 diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index 5a00144..f5ede9c 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -152,7 +152,10 @@ def load_solps_from_mdsplus(mds_server, ref_number): except (mdsExceptions.TreeNNF, TypeError): neurad = 0 - sim.total_radiation = (linerad + brmrad + neurad) / mesh.vol + total_rad = linerad + brmrad + neurad + + if isinstance(total_rad, np.ndarray): + sim.total_radiation = total_rad / mesh.vol return sim diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index 895d6b8..f9c1274 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -151,10 +151,11 @@ def load_solps_from_raw_output(simulation_path, debug=False): sim.neutral_temperature = ta / elementary_charge # Obtaining total radiation - eradt_raw_data = eirene.eradt.sum(0) - total_radiation = np.zeros((ny, nx)) - total_radiation[1:-1, 1:-1] = eradt_raw_data - sim.total_radiation = total_radiation + if eirene.eradt is not None: + eradt_raw_data = eirene.eradt.sum(0) + total_radiation = np.zeros((ny, nx)) + total_radiation[1:-1, 1:-1] = eradt_raw_data + sim.total_radiation = total_radiation sim.eirene_simulation = eirene From a7202e48823930295f18a9ff16cf7cdd9d9666e1 Mon Sep 17 00:00:00 2001 From: Vladislav Neverov Date: Sat, 19 Sep 2020 18:41:52 +0300 Subject: [PATCH 25/25] Fixes in response to reviewers comments. Removed redundant check for inside/outside mesh in SOLPSTotalRadiatedPower. --- CHANGELOG.md | 17 + cherab/solps/b2/parse_b2_block_file.py | 4 +- cherab/solps/eirene/eirene.py | 2 - cherab/solps/eirene/parser/fort44_2013.py | 1 - cherab/solps/eirene/parser/fort44_2017.py | 1 - cherab/solps/formats/balance.py | 47 +- cherab/solps/formats/mdsplus.py | 101 ++-- cherab/solps/formats/raw_simulation_files.py | 20 +- cherab/solps/mesh_geometry.py | 33 +- cherab/solps/models/radiated_power.pxd | 2 +- cherab/solps/models/radiated_power.pyx | 24 +- cherab/solps/solps_2d_functions.pyx | 6 +- cherab/solps/solps_3d_functions.pyx | 110 ---- cherab/solps/solps_plasma.py | 533 +++++++------------ 14 files changed, 329 insertions(+), 572 deletions(-) delete mode 100755 cherab/solps/solps_3d_functions.pyx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8338f33..1d671ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ Project Changelog ================= +Release 1.2.0 (19 Sept 2020) +---------------------------- + +* Added support for Raysect 0.7. +* Replaced unsafe SOLPSFunction3D and SOLPSVectorFunction3D with safe SOLPSFunction2D and SOLPSVectorFunction2D (use AxisymmetricMapper(SOLPSFunction2D) and VectorAxisymmetricMapper(SOLPSVectorFunction2D) for 3D). +* Added correct initialisation of properties in SOLPSMesh and SOLPSSimulation. +* Added new attributes to SOLPSMesh for basis vectors, cell connection areas, indices of neighbouring cells and new methods to_poloidal() and to_cartesian() for converting vectors defined on a grid from/to (poloidal, radial)/(R, Z). +* Fixed incorrect calculation of cell basis vectors. +* Inverted the indexing of data arrays and made all arrays row-major. +* Added electron_velocities and neutral_listproperties to SOLPSSimulation. +* Added 2D and 3D interpolators for plasma parameters to SOLPSSimulation. +* Added parsing of additional quantities in load_solps_from_mdsplus(), load_solps_from_raw_output() and load_solps_from_balance(). +* Added support for B2 stand-alone simulations. +* Added support for arbitrary plasma chemical composition. +* Fixed incorrect calculation of velocities. +* Other small fixes and improvements. + Release 1.1.0 (30 July 2020) ---------------------------- diff --git a/cherab/solps/b2/parse_b2_block_file.py b/cherab/solps/b2/parse_b2_block_file.py index ccda206..7c25c13 100755 --- a/cherab/solps/b2/parse_b2_block_file.py +++ b/cherab/solps/b2/parse_b2_block_file.py @@ -47,9 +47,9 @@ def _make_solps_data_object(_data): # Multiple 2D data field (e.g. na) if number > nxyg: - _data = np.array(_data).reshape((int(number / nxyg), nyg, nxg)) + _data = np.array(_data).reshape((number // nxyg, nyg, nxg)) if debug: - print('Mesh data field {} with dimensions: {:d} x {:d} x {:d}'.format(name, int(number / nxyg), nyg, nxg)) + print('Mesh data field {} with dimensions: {:d} x {:d} x {:d}'.format(name, number // nxyg, nyg, nxg)) return MESH_DATA, _data # 2D data field (e.g. ne) diff --git a/cherab/solps/eirene/eirene.py b/cherab/solps/eirene/eirene.py index f4a5da6..3eeb80e 100755 --- a/cherab/solps/eirene/eirene.py +++ b/cherab/solps/eirene/eirene.py @@ -16,8 +16,6 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -import numpy as np - # Code based on script by Felix Reimold (2016) class Eirene: diff --git a/cherab/solps/eirene/parser/fort44_2013.py b/cherab/solps/eirene/parser/fort44_2013.py index 9795f8b..9836db7 100644 --- a/cherab/solps/eirene/parser/fort44_2013.py +++ b/cherab/solps/eirene/parser/fort44_2013.py @@ -16,7 +16,6 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -import numpy as np from cherab.solps.eirene import Eirene from cherab.solps.eirene.parser.utility import read_block44 diff --git a/cherab/solps/eirene/parser/fort44_2017.py b/cherab/solps/eirene/parser/fort44_2017.py index c3fbbdd..965da3e 100644 --- a/cherab/solps/eirene/parser/fort44_2017.py +++ b/cherab/solps/eirene/parser/fort44_2017.py @@ -16,7 +16,6 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -import numpy as np from cherab.solps.eirene import Eirene from cherab.solps.eirene.parser.utility import read_block44 diff --git a/cherab/solps/formats/balance.py b/cherab/solps/formats/balance.py index e40c930..c0b8e36 100644 --- a/cherab/solps/formats/balance.py +++ b/cherab/solps/formats/balance.py @@ -20,13 +20,11 @@ import numpy as np from scipy.io import netcdf from scipy import constants -from raysect.core.math.function.float import Discrete2DMesh -from cherab.core.math.mappers import AxisymmetricMapper from cherab.core.atomic.elements import lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, eirene_flux_to_velocity, b2_flux_to_velocity def load_solps_from_balance(balance_filename): @@ -53,8 +51,8 @@ def load_solps_from_balance(balance_filename): ns = am.size for i in range(ns): symbol = ''.join([b.decode('utf-8').strip(' 0123456789+-') for b in species_names[i]]) # also strips isotope number - if symbol != 'D' and symbol != 'T': - isotope = lookup_isotope(symbol, number=am[i]) # will through an error for D or T + if symbol not in ('D', 'T'): + isotope = lookup_isotope(symbol, number=am[i]) # will throw an error for D or T species = prefer_element(isotope) # Prefer Element over Isotope if the mass number is the same else: species = lookup_isotope(symbol) @@ -71,7 +69,7 @@ def load_solps_from_balance(balance_filename): ########################## # Magnetic field vectors # sim.b_field = fhandle.variables['bb'].data.copy()[:3] - # sim.b_field_cylindrical is created authomatically + # sim.b_field_cylindrical is created automatically # Load electron species sim.electron_temperature = fhandle.variables['te'].data.copy() / el_charge @@ -87,24 +85,18 @@ def load_solps_from_balance(balance_filename): parallel_velocity = fhandle.variables['ua'].data.copy() # Load poloidal and radial particle fluxes for velocity calculation - if 'fna' in fhandle.variables: - fna = fhandle.variables['fna'].data.copy() - poloidal_flux = fna[:, 0] - radial_flux = fna[:, 1] - elif 'fnax' in fhandle.variables and 'fnay' in fhandle.variables: - poloidal_flux = fhandle.variables['fnax'].data.copy() - radial_flux = fhandle.variables['fnay'].data.copy() + if 'fna_tot' in fhandle.variables: + fna = fhandle.variables['fna_tot'].data.copy() + # Obtaining velocity from B2 flux + sim.velocities_cylindrical = b2_flux_to_velocity(sim, fna[:, 0], fna[:, 1], parallel_velocity) else: # trying to obtain particle flux from components fna = 0 for key in fhandle.variables.keys(): if 'fna_' in key: fna += fhandle.variables[key].data.copy() - if isinstance(fna, np.ndarray): - poloidal_flux = fna[:, 0] - radial_flux = fna[:, 1] - - # Obtaining velocity from B2 flux - sim.b2_flux_to_velocity(poloidal_flux, radial_flux, parallel_velocity) + if fna is not 0: + # Obtaining velocity from B2 flux + sim.velocities_cylindrical = b2_flux_to_velocity(sim, fna[:, 0], fna[:, 1], parallel_velocity) # Obtaining additional data from EIRENE and replacing data for neutrals if 'dab2' in fhandle.variables: @@ -122,7 +114,16 @@ def load_solps_from_balance(balance_filename): neutral_radial_flux = fhandle.variables['rfluxa'].data.copy()[:, :ny, :nx] if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): - sim.eirene_flux_to_velocity(neutral_poloidal_flux, neutral_radial_flux, parallel_velocity[neutral_indx]) + neutral_velocities = eirene_flux_to_velocity(sim, neutral_poloidal_flux, neutral_radial_flux, + parallel_velocity[neutral_indx]) + if sim.velocities_cylindrical is not None: + sim.velocities_cylindrical[neutral_indx] = neutral_velocities + sim.velocities_cylindrical = sim.velocities_cylindrical # Updating sim.velocities + else: + # No 'fna_*' keys in balance.nc and b2 species velocities are not set + velocities_cylindrical = np.zeros((len(sim.species_list, 3, ny, nx))) + velocities_cylindrical[neutral_indx] = neutral_velocities + sim.velocities_cylindrical = velocities_cylindrical # Obtaining neutral temperatures if 'tab2' in fhandle.variables: @@ -143,7 +144,7 @@ def load_solps_from_balance(balance_filename): # Ionisation rate from EIRENE, needed to calculate the energy loss to overcome the ionisation potential of atoms if 'eirene_mc_papl_sna_bal' in fhandle.variables: - tmp = np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=(0))[1] + tmp = np.sum(fhandle.variables['eirene_mc_papl_sna_bal'].data, axis=0)[1] eirene_potential_loss = rydberg_energy * tmp * el_charge / mesh.vol # This will be negative (energy sink); multiply by -1 @@ -163,8 +164,8 @@ def load_solps_from_balance(balance_filename): # Save total radiated power to the simulation object total_rad = rydberg_energy * el_charge * potential_loss - b2_ploss - if isinstance(total_rad, np.ndarray): - sim.total_radiation = total_rad + if total_rad is not 0: + sim.total_radiation = total_rad return sim diff --git a/cherab/solps/formats/mdsplus.py b/cherab/solps/formats/mdsplus.py index f5ede9c..09b2c5e 100644 --- a/cherab/solps/formats/mdsplus.py +++ b/cherab/solps/formats/mdsplus.py @@ -18,13 +18,10 @@ # under the Licence. import numpy as np -from raysect.core import Point2D from cherab.core.atomic.elements import lookup_isotope from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element - -from matplotlib import pyplot as plt +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, eirene_flux_to_velocity, b2_flux_to_velocity def load_solps_from_mdsplus(mds_server, ref_number): @@ -36,20 +33,20 @@ def load_solps_from_mdsplus(mds_server, ref_number): :rtype: SOLPSSimulation """ - from MDSplus import Connection as MDSConnection, mdsExceptions + from MDSplus import Connection as MDSConnection, MdsException # Setup connection to server conn = MDSConnection(mds_server) conn.openTree('solps', ref_number) # Load SOLPS mesh geometry and lookup arrays - mesh = load_mesh_from_mdsplus(conn, mdsExceptions) + mesh = load_mesh_from_mdsplus(conn, MdsException) # Load each plasma species in simulation - ns = conn.get('\SOLPS::TOP.IDENT.NS').data() # Number of species - zn = conn.get('\SOLPS::TOP.SNAPSHOT.GRID.ZN').data().astype(np.int) # Nuclear charge - am = np.round(conn.get('\SOLPS::TOP.SNAPSHOT.GRID.AM').data()).astype(np.int) # Atomic mass number - charge = conn.get('\SOLPS::TOP.SNAPSHOT.GRID.ZA').data().astype(np.int) # Ionisation/charge + ns = conn.get(r'\SOLPS::TOP.IDENT.NS').data() # Number of species + zn = conn.get(r'\SOLPS::TOP.SNAPSHOT.GRID.ZN').data().astype(np.int) # Nuclear charge + am = np.round(conn.get(r'\SOLPS::TOP.SNAPSHOT.GRID.AM').data()).astype(np.int) # Atomic mass number + charge = conn.get(r'\SOLPS::TOP.SNAPSHOT.GRID.ZA').data().astype(np.int) # Ionisation/charge species_list = [] neutral_indx = [] @@ -66,25 +63,25 @@ def load_solps_from_mdsplus(mds_server, ref_number): ########################## # Magnetic field vectors # - sim.b_field = conn.get('\SOLPS::TOP.SNAPSHOT.B').data()[:3] - # sim.b_field_cylindrical is created authomatically + sim.b_field = conn.get(r'\SOLPS::TOP.SNAPSHOT.B').data()[:3] + # sim.b_field_cylindrical is created automatically # Load electron temperature and density - sim.electron_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TE').data() - sim.electron_density = conn.get('\SOLPS::TOP.SNAPSHOT.NE').data() + sim.electron_temperature = conn.get(r'\SOLPS::TOP.SNAPSHOT.TE').data() + sim.electron_density = conn.get(r'\SOLPS::TOP.SNAPSHOT.NE').data() # Load ion temperature - sim.ion_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TI').data() + sim.ion_temperature = conn.get(r'\SOLPS::TOP.SNAPSHOT.TI').data() # Load species density - sim.species_density = conn.get('\SOLPS::TOP.SNAPSHOT.NA').data() + sim.species_density = conn.get(r'\SOLPS::TOP.SNAPSHOT.NA').data() # Load parallel velocity - parallel_velocity = conn.get('\SOLPS::TOP.SNAPSHOT.UA').data() + parallel_velocity = conn.get(r'\SOLPS::TOP.SNAPSHOT.UA').data() # Load poloidal and radial particle fluxes for velocity calculation - poloidal_flux = conn.get('\SOLPS::TOP.SNAPSHOT.FNAX').data() - radial_flux = conn.get('\SOLPS::TOP.SNAPSHOT.FNAY').data() + poloidal_flux = conn.get(r'\SOLPS::TOP.SNAPSHOT.FNAX').data() + radial_flux = conn.get(r'\SOLPS::TOP.SNAPSHOT.FNAY').data() # B2 fluxes are defined between cells, so correcting array shapes if needed if poloidal_flux.shape[2] == nx - 1: @@ -94,37 +91,41 @@ def load_solps_from_mdsplus(mds_server, ref_number): radial_flux = np.concatenate((np.zeros((ns, 1, nx)), radial_flux), axis=1) # Setting velocities from B2 flux - sim.b2_flux_to_velocity(poloidal_flux, radial_flux, parallel_velocity) + sim.velocities_cylindrical = b2_flux_to_velocity(sim, poloidal_flux, radial_flux, parallel_velocity) # Obtaining additional data from EIRENE and replacing data for neutrals - b2_standalone = False try: # Replace the species densities - neutral_density = conn.get('\SOLPS::TOP.SNAPSHOT.DAB2').data() # this will throw a TypeError is neutral_density is not an array + neutral_density = conn.get(r'\SOLPS::TOP.SNAPSHOT.DAB2').data() # this will throw a TypeError is neutral_density is not an array # We can update the data without re-initialising interpolators because they use pointers sim.species_density[neutral_indx] = neutral_density[:] - except (mdsExceptions.TreeNNF, TypeError): + except (MdsException, TypeError): print("Warning! This is B2 stand-alone simulation.") b2_standalone = True + else: + b2_standalone = False if not b2_standalone: # Obtaining neutral atom velocity from EIRENE flux # Note that if the output for fluxes was turned off, PFLA and RFLA' are all zeros try: - neutral_poloidal_flux = conn.get('\SOLPS::TOP.SNAPSHOT.PFLA').data()[:] - neutral_radial_flux = conn.get('\SOLPS::TOP.SNAPSHOT.RFLA').data()[:] + neutral_poloidal_flux = conn.get(r'\SOLPS::TOP.SNAPSHOT.PFLA').data()[:] + neutral_radial_flux = conn.get(r'\SOLPS::TOP.SNAPSHOT.RFLA').data()[:] if np.any(neutral_poloidal_flux) or np.any(neutral_radial_flux): - sim.eirene_flux_to_velocity(neutral_poloidal_flux, neutral_radial_flux, parallel_velocity[neutral_indx]) - except (mdsExceptions.TreeNNF, TypeError): + sim.velocities_cylindrical[neutral_indx] = eirene_flux_to_velocity(sim, neutral_poloidal_flux, neutral_radial_flux, + parallel_velocity[neutral_indx]) + sim.velocities_cylindrical = sim.velocities_cylindrical # Updating sim.velocities + + except (MdsException, TypeError): pass # Obtaining neutral temperatures try: - sim.neutral_temperature = conn.get('\SOLPS::TOP.SNAPSHOT.TAB2').data()[:] - except (mdsExceptions.TreeNNF, TypeError): + sim.neutral_temperature = conn.get(r'\SOLPS::TOP.SNAPSHOT.TAB2').data()[:] + except (MdsException, TypeError): pass ############################### @@ -134,33 +135,33 @@ def load_solps_from_mdsplus(mds_server, ref_number): #################### # Integrated power # try: - linerad = np.sum(conn.get('\SOLPS::TOP.SNAPSHOT.RQRAD').data()[:], axis=0) - except (mdsExceptions.TreeNNF, TypeError): + linerad = np.sum(conn.get(r'\SOLPS::TOP.SNAPSHOT.RQRAD').data()[:], axis=0) + except (MdsException, TypeError): linerad = 0 try: - brmrad = np.sum(conn.get('\SOLPS::TOP.SNAPSHOT.RQBRM').data()[:], axis=0) - except (mdsExceptions.TreeNNF, TypeError): + brmrad = np.sum(conn.get(r'\SOLPS::TOP.SNAPSHOT.RQBRM').data()[:], axis=0) + except (MdsException, TypeError): brmrad = 0 try: - eneutrad = conn.get('\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data()[:] + eneutrad = conn.get(r'\SOLPS::TOP.SNAPSHOT.ENEUTRAD').data()[:] if np.ndim(eneutrad) == 3: neurad = np.abs(np.sum(eneutrad, axis=0)) else: neurad = np.abs(eneutrad) - except (mdsExceptions.TreeNNF, TypeError): + except (MdsException, TypeError): neurad = 0 total_rad = linerad + brmrad + neurad - if isinstance(total_rad, np.ndarray): + if total_rad is not 0: sim.total_radiation = total_rad / mesh.vol return sim -def load_mesh_from_mdsplus(mds_connection, mdsExceptions): +def load_mesh_from_mdsplus(mds_connection, MdsException): """ Load the SOLPS mesh geometry for a given MDSplus connection. @@ -169,24 +170,24 @@ def load_mesh_from_mdsplus(mds_connection, mdsExceptions): """ # Load the R, Z coordinates of the cell vertices, original coordinates are (4, 38, 98) - r = mds_connection.get('\TOP.SNAPSHOT.GRID:R').data() - z = mds_connection.get('\TOP.SNAPSHOT.GRID:Z').data() + r = mds_connection.get(r'\TOP.SNAPSHOT.GRID:R').data() + z = mds_connection.get(r'\TOP.SNAPSHOT.GRID:Z').data() - vol = mds_connection.get('\SOLPS::TOP.SNAPSHOT.VOL').data() + vol = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.VOL').data() # Loading neighbouring cell indices neighbix = np.zeros(r.shape, dtype=np.int) neighbiy = np.zeros(r.shape, dtype=np.int) - neighbix[0] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIX').data().astype(np.int) - neighbix[1] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIX').data().astype(np.int) - neighbix[2] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIX').data().astype(np.int) - neighbix[3] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIX').data().astype(np.int) + neighbix[0] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:LEFTIX').data().astype(np.int) + neighbix[1] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIX').data().astype(np.int) + neighbix[2] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIX').data().astype(np.int) + neighbix[3] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:TOPIX').data().astype(np.int) - neighbiy[0] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:LEFTIY').data().astype(np.int) - neighbiy[1] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIY').data().astype(np.int) - neighbiy[2] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIY').data().astype(np.int) - neighbiy[3] = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:TOPIY').data().astype(np.int) + neighbiy[0] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:LEFTIY').data().astype(np.int) + neighbiy[1] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:BOTTOMIY').data().astype(np.int) + neighbiy[2] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:RIGHTIY').data().astype(np.int) + neighbiy[3] = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:TOPIY').data().astype(np.int) neighbix[neighbix == r.shape[2]] = -1 neighbiy[neighbiy == r.shape[1]] = -1 @@ -200,9 +201,9 @@ def load_mesh_from_mdsplus(mds_connection, mdsExceptions): # add the vessel geometry try: - vessel = mds_connection.get('\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data()[:] + vessel = mds_connection.get(r'\SOLPS::TOP.SNAPSHOT.GRID:VESSEL').data()[:] mesh.vessel = vessel - except (mdsExceptions.TreeNNF, TypeError): + except (MdsException, TypeError): pass return mesh diff --git a/cherab/solps/formats/raw_simulation_files.py b/cherab/solps/formats/raw_simulation_files.py index f9c1274..244338c 100644 --- a/cherab/solps/formats/raw_simulation_files.py +++ b/cherab/solps/formats/raw_simulation_files.py @@ -20,15 +20,13 @@ import os import numpy as np from scipy.constants import elementary_charge -from raysect.core.math.function.float import Discrete2DMesh -from cherab.core.math.mappers import AxisymmetricMapper from cherab.core.atomic.elements import lookup_isotope from cherab.solps.eirene import load_fort44_file from cherab.solps.b2.parse_b2_block_file import load_b2f_file from cherab.solps.mesh_geometry import SOLPSMesh -from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element +from cherab.solps.solps_plasma import SOLPSSimulation, prefer_element, eirene_flux_to_velocity, b2_flux_to_velocity # Code based on script by Felix Reimold (2016) @@ -46,7 +44,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): """ if not os.path.isdir(simulation_path): - RuntimeError("Simulation_path must be a valid directory.") + raise RuntimeError("simulation_path must be a valid directory.") mesh_file_path = os.path.join(simulation_path, 'b2fgmtry') b2_state_file = os.path.join(simulation_path, 'b2fstate') @@ -55,10 +53,10 @@ def load_solps_from_raw_output(simulation_path, debug=False): if not os.path.isfile(mesh_file_path): raise RuntimeError("No B2 b2fgmtry file found in SOLPS output directory.") - if not(os.path.isfile(b2_state_file)): - RuntimeError("No B2 b2fstate file found in SOLPS output directory.") + if not os.path.isfile(b2_state_file): + raise RuntimeError("No B2 b2fstate file found in SOLPS output directory.") - if not(os.path.isfile(eirene_fort44_file)): + if not os.path.isfile(eirene_fort44_file): print("Warning! No EIRENE fort.44 file found in SOLPS output directory. Assuming B2 stand-alone simulation.") b2_standalone = True else: @@ -94,7 +92,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): # Load magnetic field sim.b_field = geom_data_dict['bb'][:3] - # sim.b_field_cylindrical is created authomatically + # sim.b_field_cylindrical is created automatically # Load electron species sim.electron_temperature = mesh_data_dict['te'] / elementary_charge @@ -114,7 +112,7 @@ def load_solps_from_raw_output(simulation_path, debug=False): radial_flux = mesh_data_dict['fna'][1::2] # Obtaining velocity from B2 flux - sim.b2_flux_to_velocity(poloidal_flux, radial_flux, parallel_velocity) + sim.velocities_cylindrical = b2_flux_to_velocity(sim, poloidal_flux, radial_flux, parallel_velocity) if not b2_standalone: # Obtaining additional data from EIRENE and replacing data for neutrals @@ -137,7 +135,9 @@ def load_solps_from_raw_output(simulation_path, debug=False): neutral_parallel_velocity = np.zeros((len(neutral_indx), ny, nx)) # must be zero outside EIRENE grid neutral_parallel_velocity[:, 1:-1, 1:-1] = parallel_velocity[neutral_indx, 1:-1, 1:-1] - sim.eirene_flux_to_velocity(neutral_poloidal_flux, neutral_radial_flux, neutral_parallel_velocity) + sim.velocities_cylindrical[neutral_indx] = eirene_flux_to_velocity(sim, neutral_poloidal_flux, neutral_radial_flux, + neutral_parallel_velocity) + sim.velocities_cylindrical = sim.velocities_cylindrical # Updating sim.velocities # Obtaining neutral temperatures ta = np.zeros((eirene.ta.shape[0], ny, nx)) diff --git a/cherab/solps/mesh_geometry.py b/cherab/solps/mesh_geometry.py index 4e82c9b..5cab148 100755 --- a/cherab/solps/mesh_geometry.py +++ b/cherab/solps/mesh_geometry.py @@ -17,14 +17,12 @@ # See the Licence for the specific language governing permissions and limitations # under the Licence. -# External imports -# from collections import namedtuple - import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Polygon from matplotlib.collections import PatchCollection + class SOLPSMesh: """ SOLPSMesh geometry object. @@ -43,25 +41,25 @@ class SOLPSMesh: must be 3 dimensional. Example shape is (4 x 32 x 98). In SOLPS notation: left/right - poloidal prev./next, bottom/top - radial prev./next. Cell indexing starts with 0 and -1 means no neighbour. - :param ndarray neighbix: Array of radial indeces of neighbouring cells in order: left, bottom, right, top, + :param ndarray neighbiy: Array of radial indeces of neighbouring cells in order: left, bottom, right, top, must be 3 dimensional. Example shape is (4 x 32 x 98). """ - # TODO Make neighbix and neighbix optional in the future, as they can be reconstructed with _tri_index_loopup + # TODO Make neighbix and neighbix optional in the future, as they can be reconstructed with _triangle_to_grid_map def __init__(self, r, z, vol, neighbix, neighbiy): if r.shape != z.shape: - raise ValueError('Shape of r array: %s mismatch the shape of z array: %s.' % (r.shape, z.shape)) + raise ValueError('Shape of r array: {0} mismatch the shape of z array: {1}.'.format(r.shape, z.shape)) if vol.shape != r.shape[1:]: - raise ValueError('Shape of vol array: %s mismatch the grid dimentions: %s.' % (vol.shape, r.shape[1:])) + raise ValueError('Shape of vol array: {0} mismatch the grid dimentions: {1}.'.format(vol.shape, r.shape[1:])) if neighbix.shape != r.shape: - raise ValueError('Shape of neighbix array must be %s, but it is %s.' % (r.shape, neighbix.shape)) + raise ValueError('Shape of neighbix array must be {0}, but it is {1}.'.format(r.shape, neighbix.shape)) if neighbiy.shape != r.shape: - raise ValueError('Shape of neighbix array must be %s, but it is %s.' % (r.shape, neighbiy.shape)) + raise ValueError('Shape of neighbix array must be {0}, but it is {}.'.format(r.shape, neighbiy.shape)) self._cr = r.sum(0) / 4. self._cz = z.sum(0) / 4. @@ -102,18 +100,10 @@ def __init__(self, r, z, vol, neighbix, neighbiy): self._poloidal_area = np.pi * (r[2] + r[0]) * vec_magn # For convertion from Cartesian to poloidal - # TODO Make it work with trianle cells + # TODO Make it work with triangle cells self._inv_det = 1. / (self._poloidal_basis_vector[0] * self._radial_basis_vector[1] - self._poloidal_basis_vector[1] * self._radial_basis_vector[0]) - # Test for basis vector calculation - # plt.quiver(self._cr[0], self._cz[0], self._radial_basis_vector[0, 0], self._radial_basis_vector[1, 0], color='k') - # plt.quiver(self._cr[0], self._cz[0], self._poloidal_basis_vector[0, 0], self._poloidal_basis_vector[1, 0], color='r') - # plt.quiver(self._cr[-1], self._cz[-1], self._radial_basis_vector[0, -1], self._radial_basis_vector[1, -1], color='k') - # plt.quiver(self._cr[-1], self._cz[-1], self._poloidal_basis_vector[0, -1], self._poloidal_basis_vector[1, -1], color='r') - # plt.gca().set_aspect('equal') - # plt.show() - # Finding unique vertices vertices = np.array([r.flatten(), z.flatten()]).T self._vertex_coords, unique_vertices = np.unique(vertices, axis=0, return_inverse=True) @@ -314,11 +304,4 @@ def plot_mesh(self): ax.add_collection(p) ax.axis('equal') - # Code for plotting vessel geometry if available - # if self.vessel is not None: - # for i in range(self.vessel.shape[0]): - # ax.plot([self.vessel[i, 0], self.vessel[i, 2]], [self.vessel[i, 1], self.vessel[i, 3]], 'k') - # for i in range(self.vessel.shape[0]): - # ax.plot([self.vessel[i, 0], self.vessel[i, 2]], [self.vessel[i, 1], self.vessel[i, 3]], 'or') - return ax diff --git a/cherab/solps/models/radiated_power.pxd b/cherab/solps/models/radiated_power.pxd index 89cae88..0dd7293 100755 --- a/cherab/solps/models/radiated_power.pxd +++ b/cherab/solps/models/radiated_power.pxd @@ -29,7 +29,7 @@ cdef class SOLPSTotalRadiatedPower(InhomogeneousVolumeEmitter): cdef: public double vertical_offset - Function3D total_rad, inside_simulation + Function3D total_rad cpdef Spectrum emission_function(self, Point3D point, Vector3D direction, Spectrum spectrum, World world, Ray ray, Primitive primitive, diff --git a/cherab/solps/models/radiated_power.pyx b/cherab/solps/models/radiated_power.pyx index 72ae0f8..f7f5566 100755 --- a/cherab/solps/models/radiated_power.pyx +++ b/cherab/solps/models/radiated_power.pyx @@ -30,19 +30,15 @@ cimport cython cdef class SOLPSTotalRadiatedPower(InhomogeneousVolumeEmitter): - def __init__(self, object solps_simulation, double vertical_offset=0.0, step=0.01): + def __init__(self, Function3D total_radiation, double vertical_offset=0.0, step=0.01): super().__init__(NumericalIntegrator(step=step)) self.vertical_offset = vertical_offset - self.inside_simulation = solps_simulation.inside_volume_mesh - self.total_rad = solps_simulation.total_radiation_f3d + self.total_rad = total_radiation - def __call__(self, x, y , z): + def __call__(self, x, y, z): - if self.inside_simulation.evaluate(x, y, z) < 1.0: - return 0.0 - - return self.total_rad.evaluate(x, y, z) + return self.total_rad.evaluate(x, y, z) # this returns 0 if outside the mesh @cython.boundscheck(False) @cython.wraparound(False) @@ -54,16 +50,18 @@ cdef class SOLPSTotalRadiatedPower(InhomogeneousVolumeEmitter): cdef: double offset_z, wvl_range - offset_z = point.z + self.vertical_offset - if self.inside_simulation.evaluate(point.x, point.y, offset_z) < 1.0: - return spectrum + offset_z = point.z + self.vertical_offset wvl_range = ray.max_wavelength - ray.min_wavelength spectrum.samples_mv[:] = self.total_rad.evaluate(point.x, point.y, offset_z) / (4 * pi * wvl_range * spectrum.bins) return spectrum + def solps_total_radiated_power(world, solps_simulation, step=0.01): + if solps_simulation.total_radiation_f3d is None: + raise RuntimeError('Total radiation is not available for this simulation.') + mesh = solps_simulation.mesh outer_radius = mesh.mesh_extent['maxr'] inner_radius = mesh.mesh_extent['minr'] - 0.001 @@ -71,9 +69,7 @@ def solps_total_radiated_power(world, solps_simulation, step=0.01): lower_z = mesh.mesh_extent['minz'] main_plasma_cylinder = Cylinder(outer_radius, plasma_height, parent=world, - material=SOLPSTotalRadiatedPower(solps_simulation, vertical_offset=lower_z, step=step), + material=SOLPSTotalRadiatedPower(solps_simulation.total_radiation_f3d, vertical_offset=lower_z, step=step), transform=translate(0, 0, lower_z)) return main_plasma_cylinder - - diff --git a/cherab/solps/solps_2d_functions.pyx b/cherab/solps/solps_2d_functions.pyx index 1e1aa0f..6b40f78 100755 --- a/cherab/solps/solps_2d_functions.pyx +++ b/cherab/solps/solps_2d_functions.pyx @@ -114,7 +114,8 @@ cdef class SOLPSFunction2D(Function2D): if self._kdtree.is_contained(new_point2d(x, y)): triangle_id = self._kdtree.triangle_id - ix, iy = self._triangle_to_grid_map_mv[triangle_id, :] + ix = self._triangle_to_grid_map_mv[triangle_id, 0] + iy = self._triangle_to_grid_map_mv[triangle_id, 1] return self._grid_data_mv[ix, iy] return 0.0 @@ -204,7 +205,8 @@ cdef class SOLPSVectorFunction2D(VectorFunction2D): if self._kdtree.is_contained(new_point2d(x, y)): triangle_id = self._kdtree.triangle_id - ix, iy = self._triangle_to_grid_map_mv[triangle_id, :] + ix = self._triangle_to_grid_map_mv[triangle_id, 0] + iy = self._triangle_to_grid_map_mv[triangle_id, 1] vx = self._grid_vectors_mv[0, ix, iy] vy = self._grid_vectors_mv[1, ix, iy] vz = self._grid_vectors_mv[2, ix, iy] diff --git a/cherab/solps/solps_3d_functions.pyx b/cherab/solps/solps_3d_functions.pyx deleted file mode 100755 index 4d8b68d..0000000 --- a/cherab/solps/solps_3d_functions.pyx +++ /dev/null @@ -1,110 +0,0 @@ -# cython: language_level=3 - -# Copyright 2016-2018 Euratom -# Copyright 2016-2018 United Kingdom Atomic Energy Authority -# Copyright 2016-2018 Centro de Investigaciones Energéticas, Medioambientales y Tecnológicas -# -# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the -# European Commission - subsequent versions of the EUPL (the "Licence"); -# You may not use this work except in compliance with the Licence. -# You may obtain a copy of the Licence at: -# -# https://joinup.ec.europa.eu/software/page/eupl5 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. -# -# See the Licence for the specific language governing permissions and limitations -# under the Licence. - -from libc.math cimport atan2, M_PI -import numpy as np -from numpy cimport ndarray - -from raysect.core.math.vector cimport Vector3D, new_vector3d -from raysect.core.math.transform cimport rotate_z - -from cherab.core.math.mappers cimport AxisymmetricMapper -from cherab.core.math.function cimport Function3D, Discrete2DMesh, VectorFunction3D -cimport cython - -cdef double RAD_TO_DEG = 360 / (2*M_PI) - - -cdef class SOLPSFunction3D(Function3D): - - cdef: - AxisymmetricMapper _triangle_index_lookup - int[:,:] _triangle_to_grid_map - double[:,:] _grid_values - - def __init__(self, Discrete2DMesh triangle_index_lookup, int[:,:] triangle_to_grid_map, double[:,:] grid_values): - - # todo: this is unsafe - converting an int to a double and performing operations - rounding errors - self._triangle_index_lookup = AxisymmetricMapper(triangle_index_lookup) - self._triangle_to_grid_map = triangle_to_grid_map - self._grid_values = grid_values - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - cdef double evaluate(self, double x, double y, double z) except? -1e999: - - cdef: - int tri_index # Index of the underlying mesh triangle - int ix # SOLPS grid x coordinate - int iy # SOLPS grid y coordinate - - try: - tri_index = self._triangle_index_lookup.evaluate(x, y, z) - except ValueError: - return 0.0 # Return zero if outside of mesh bounds - - ix, iy = self._triangle_to_grid_map[tri_index, :] - - return self._grid_values[ix, iy] - - -cdef class SOLPSVectorFunction3D(VectorFunction3D): - - cdef: - AxisymmetricMapper _triangle_index_lookup - int[:,:] _triangle_to_grid_map - double[:,:,:] _grid_vectors - - def __init__(self, Discrete2DMesh triangle_index_lookup, object triangle_to_grid_map, object grid_vectors): - - # todo: this is unsafe - converting an int to a double and performing operations - rounding errors - self._triangle_index_lookup = AxisymmetricMapper(triangle_index_lookup) - self._triangle_to_grid_map = triangle_to_grid_map - self._grid_vectors = grid_vectors - - @cython.boundscheck(False) - @cython.wraparound(False) - @cython.initializedcheck(False) - cdef Vector3D evaluate(self, double x, double y, double z): - - cdef: - int tri_index # Index of the underlying mesh triangle - int ix # SOLPS grid x coordinate - int iy # SOLPS grid y coordinate - double vx, vy, vz - Vector3D v - - try: - tri_index = self._triangle_index_lookup.evaluate(x, y, z) - except ValueError: - return Vector3D(0.0, 0.0, 0.0) # Return zero vector if outside of mesh bounds. - - ix, iy = self._triangle_to_grid_map[tri_index, :] - - # print(self._grid_vectors.shape) - # Lookup vector for this grid cell. - vx = self._grid_vectors[0, ix, iy] - vy = self._grid_vectors[1, ix, iy] - vz = self._grid_vectors[2, ix, iy] - v = new_vector3d(vx, vy, vz) - - # Rotate vector field around the z-axis. - return v.transform(rotate_z(atan2(y, x) * RAD_TO_DEG)) diff --git a/cherab/solps/solps_plasma.py b/cherab/solps/solps_plasma.py index 759402e..f2bac0a 100755 --- a/cherab/solps/solps_plasma.py +++ b/cherab/solps/solps_plasma.py @@ -19,16 +19,15 @@ import pickle import numpy as np -import matplotlib.pyplot as plt -from scipy.constants import atomic_mass, electron_mass, elementary_charge +from scipy.constants import atomic_mass, electron_mass # Raysect imports -from raysect.core import translate, Point3D, Vector3D, Node, AffineMatrix3D +from raysect.core import translate, Vector3D from raysect.primitive import Cylinder -from raysect.optical import Spectrum # CHERAB core imports from cherab.core import Plasma, Species, Maxwellian +from cherab.core.math.function import ConstantVector3D from cherab.core.math.mappers import AxisymmetricMapper, VectorAxisymmetricMapper from cherab.core.atomic.elements import lookup_isotope, lookup_element @@ -91,9 +90,9 @@ def __init__(self, mesh, species_list): self._b_field_cylindrical = None self._b_field_cylindrical_f2d = None self._b_field_cartesian = None - self._eirene_model = None # what is this for? - self._b2_model = None # what is this for? - self._eirene = None # do we need this in SOLPSSimulation? + self._eirene_model = None + self._b2_model = None + self._eirene = None @property def mesh(self): @@ -111,6 +110,14 @@ def species_list(self): """ return self._species_list + @property + def neutral_list(self): + """ + Tuple of species elements in the form (species name, charge). + :return: + """ + return self._neutral_list + @property def electron_temperature(self): """ @@ -438,7 +445,7 @@ def velocities_cylindrical_f2d(self): Dictionary of VectorFunction2D interpolators for species velocities in cylindrical coordinates. Accessed by species index or species_list elements. - E.g., elocities_cylindrical_f2d[1] or elocities_cylindrical_f2d[('deuterium', 1))]. + E.g., velocities_cylindrical_f2d[1] or velocities_cylindrical_f2d[('deuterium', 1))]. Each entry returns a vector of species velocity at a given point (R, Z). :return: """ @@ -450,7 +457,7 @@ def velocities_cartesian(self): Dictionary of VectorFunction3D interpolators for species velocities in Cartesian coordinates. Accessed by species index or species_list elements. - E.g., elocities_cylindrical_f3d[1] or elocities_cylindrical_f3d[('deuterium', 1))]. + E.g., velocities_cartesian[1] or velocities_cartesian[('deuterium', 1))]. Each entry returns a vector of species velocity at a given point (x, y, z). :return: """ @@ -477,13 +484,11 @@ def total_radiation(self): Array of shape (ny, nx). This is not calculated from the CHERAB emission models, instead it comes from the SOLPS output data. - Is calculated from the sum of all integrated line emission and all Bremmstrahlung. The signals used are 'RQRAD' - and 'RQBRM'. Final output is in W/str? + Is calculated from the sum of all integrated line emission and all Bremmstrahlung. + The signals used are 'RQRAD' and 'RQBRM'. Final output is in W m-3. """ - if self._total_radiation is None: - raise RuntimeError("Total radiation not available for this simulation.") - else: - return self._total_radiation + + return self._total_radiation @property def total_radiation_f2d(self): @@ -491,10 +496,8 @@ def total_radiation_f2d(self): Function2D interpolator for total radiation. Returns total radiation at a given point (R, Z). """ - if self._total_radiation_f2d is None: - raise RuntimeError("Total radiation not available for this simulation.") - else: - return self._total_radiation_f2d + + return self._total_radiation_f2d @property def total_radiation_f3d(self): @@ -502,10 +505,8 @@ def total_radiation_f3d(self): Function3D interpolator for total radiation. Returns total radiation at a given point (x, y, z). """ - if self._total_radiation_f3d is None: - raise RuntimeError("Total radiation not available for this simulation.") - else: - return self._total_radiation_f3d + + return self._total_radiation_f3d @total_radiation.setter def total_radiation(self, value): @@ -521,10 +522,8 @@ def b_field(self): Magnetic B field in poloidal coordinates (e_pol, e_rad, e_tor) at each mesh cell. Array of shape (3, ny, nx): [0, :, :] - poloidal, [1, :, :] - radial, [2, :, :] - toroidal. """ - if self._b_field is None: - raise RuntimeError("Magnetic field not available for this simulation.") - else: - return self._b_field + + return self._b_field @b_field.setter def b_field(self, value): @@ -547,10 +546,8 @@ def b_field_cylindrical(self): Magnetic B field in poloidal coordinates (R, phi, Z) at each mesh cell. Array of shape (3, ny, nx): [0, :, :] - R, [1, :, :] - phi, [2, :, :] - Z. """ - if self._b_field_cylindrical is None: - raise RuntimeError("Magnetic field not available for this simulation.") - else: - return self._b_field_cylindrical + + return self._b_field_cylindrical @property def b_field_cylindrical_f2d(self): @@ -558,21 +555,17 @@ def b_field_cylindrical_f2d(self): VectorFunction2D interpolator for magnetic B field in cylindrical coordinates. Returns a vector of magnetic field at a given point (R, Z). """ - if self._b_field_cylindrical_f2d is None: - raise RuntimeError("Magnetic field not available for this simulation.") - else: - return self._b_field_cylindrical_f2d + + return self._b_field_cylindrical_f2d @property def b_field_cartesian(self): """ - VectorFunction2D interpolator for magnetic B field in Cartesian coordinates. + VectorFunction3D interpolator for magnetic B field in Cartesian coordinates. Returns a vector of magnetic field at a given point (x, y, z). """ - if self._b_field_cartesian is None: - raise RuntimeError("Magnetic field not available for this simulation.") - else: - return self._b_field_cartesian + + return self._b_field_cartesian @b_field_cylindrical.setter def b_field_cylindrical(self, value): @@ -596,10 +589,8 @@ def eirene_simulation(self): :rtype: Eirene """ - if self._eirene is None: - raise RuntimeError("EIRENE simulation data not available for this SOLPS simulation.") - else: - return self._eirene + + return self._eirene @eirene_simulation.setter def eirene_simulation(self, value): @@ -608,152 +599,6 @@ def eirene_simulation(self, value): self._eirene = value - def b2_flux_to_velocity(self, poloidal_flux, radial_flux, parallel_velocity): - """ - Sets velocities of all species using B2 particle fluxes defined at cell faces. - - :param ndarray poloidal_flux: Poloidal flux of atoms in s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray radial_flux: Radial flux of atoms in s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional - array of shape (num_atoms, mesh.ny, mesh.nx). - Parallel velocity is a velocity projection on magnetic - field direction. - """ - if self.b_field_cylindrical is None: - raise RuntimeError('Attribute "b_field_cylindrical" is not set.') - if self.species_density is None: - raise RuntimeError('Attribute "species_density" is not set.') - - mesh = self.mesh - b = self.b_field_cylindrical - - poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) - radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) - parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) - - nx = mesh.nx # poloidal - ny = mesh.ny # radial - ns = len(self.species_list) # number of species - - _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) - _check_shape('radial_flux', radial_flux, (ns, ny, nx)) - _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) - - poloidal_area = mesh.poloidal_area - radial_area = mesh.radial_area - leftix = mesh.neighbix[0] # poloidal prev. - leftiy = mesh.neighbiy[0] - bottomix = mesh.neighbix[1] # radial prev. - bottomiy = mesh.neighbiy[1] - rightix = mesh.neighbix[2] # poloidal next. - rightiy = mesh.neighbiy[2] - topix = mesh.neighbix[3] # radial next. - topiy = mesh.neighbiy[3] - - # Converting s-1 --> m-2 s-1 - poloidal_flux = np.divide(poloidal_flux, poloidal_area, out=np.zeros_like(poloidal_flux), where=poloidal_area > 0) - radial_flux = np.divide(radial_flux, radial_area, out=np.zeros_like(radial_flux), where=radial_area > 0) - - # Obtaining left velocity - dens_neighb = self.species_density[:, leftiy, leftix] # density in the left neighbouring cell - has_neighbour = ((leftix > -1) * (leftiy > -1)) # check if has left neighbour - neg_flux = (poloidal_flux < 0) * (self.species_density > 0) # will use density in this cell if flux is negative - pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour # will use density in neighbouring cell if flux is positive - velocity_left = np.divide(poloidal_flux, self.species_density, out=np.zeros((ns, ny, nx)), where=neg_flux) - velocity_left = np.divide(poloidal_flux, dens_neighb, out=velocity_left, where=pos_flux) - poloidal_face_normal = mesh.radial_basis_vector[[1, 0]] - poloidal_face_normal[1] *= -1 - velocity_left = velocity_left[:, None] * poloidal_face_normal # to vector in RZ - - # Obtaining bottom velocity - dens_neighb = self.species_density[:, bottomiy, bottomix] - has_neighbour = ((bottomix > -1) * (bottomiy > -1)) - neg_flux = (radial_flux < 0) * (self.species_density > 0) - pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour - velocity_bottom = np.divide(radial_flux, self.species_density, out=np.zeros((ns, ny, nx)), where=neg_flux) - velocity_bottom = np.divide(radial_flux, dens_neighb, out=velocity_bottom, where=pos_flux) - radial_face_normal = mesh.poloidal_basis_vector[[1, 0]] - radial_face_normal[0] *= -1 - velocity_bottom = velocity_bottom[:, None] * radial_face_normal # to RZ - - # Obtaining right and top velocities - velocity_right = velocity_left[:, :, rightiy, rightix] - velocity_right[:, :, (rightix < 0) + (rightiy < 0)] = 0 - - velocity_top = velocity_bottom[:, :, topiy, topix] - velocity_top[:, :, (topix < 0) + (topiy < 0)] = 0 - - vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates - - # Projection of velocity on RZ-plane - vcyl[:, [0, 2]] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) - - # Obtaining toroidal velocity - bmagn = np.sqrt((b * b).sum(0)) - vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[0] - vcyl[:, 2] * b[2]) / b[1] - - self.velocities_cylindrical = vcyl - - def eirene_flux_to_velocity(self, poloidal_flux, radial_flux, parallel_velocity): - """ - Sets velocities of neutral atoms using Eirene particle fluxes defined at cell centre. - - :param ndarray poloidal_flux: Poloidal flux of atoms in m-2 s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray radial_flux: Radial flux of atoms in m-2 s-1. Must be a 3 dimensional array of - shape (num_atoms, mesh.ny, mesh.nx). - :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional - array of shape (num_atoms, mesh.ny, mesh.nx). - Parallel velocity is a velocity projection on magnetic - field direction. - """ - if self.b_field_cylindrical is None: - raise RuntimeError('Attribute "b_field_cylindrical" is not set.') - if self.species_density is None: - raise RuntimeError('Attribute "species_density" is not set.') - - mesh = self.mesh - b = self.b_field_cylindrical - - poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) - radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) - parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) - - nx = mesh.nx # poloidal - ny = mesh.ny # radial - ns = len(self._neutral_list) # number of neutral atoms - - _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) - _check_shape('radial_flux', radial_flux, (ns, ny, nx)) - _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) - - neutral_indx = [k for k, sp in enumerate(self.species_list) if sp[1] == 0] - density = self.species_density[neutral_indx, :] - - # Obtaining velocity - poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) - radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) - - vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates - - # Projection of velocity on RZ-plane - vcyl[:, [0, 2]] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector + radial_velocity[:, None] * mesh.radial_basis_vector) - - # Obtaining toroidal velocity - bmagn = np.sqrt((b * b).sum(0)) - vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[0] - vcyl[:, 2] * b[2]) / b[1] - - if self.velocities_cylindrical is not None: # we need to update self._velocities also - vcyl_all = self.velocities_cylindrical - else: - vcyl_all = np.zeros(len(self.species_list, 3, ny, nx)) - - vcyl_all[neutral_indx] = vcyl - - self.velocities_cylindrical = vcyl_all - def __getstate__(self): state = { 'mesh': self._mesh.__getstate__(), @@ -802,140 +647,6 @@ def save(self, filename): pickle.dump(self.__getstate__(), file_handle) file_handle.close() - # def plot_electrons(self): - # """ Make a plot of the electron temperature and density in the SOLPS mesh plane. """ - # - # me = self.mesh.mesh_extent - # xl, xu = (me['minr'], me['maxr']) - # yl, yu = (me['minz'], me['maxz']) - # - # te_samples = np.zeros((500, 500)) - # ne_samples = np.zeros((500, 500)) - # - # xrange = np.linspace(xl, xu, 500) - # yrange = np.linspace(yl, yu, 500) - # - # plasma = self.plasma - # for i, x in enumerate(xrange): - # for j, y in enumerate(yrange): - # ne_samples[j, i] = plasma.electron_distribution.density(x, 0.0, y) - # te_samples[j, i] = plasma.electron_distribution.effective_temperature(x, 0.0, y) - # - # plt.figure() - # plt.imshow(ne_samples, extent=[xl, xu, yl, yu], origin='lower') - # plt.colorbar() - # plt.xlim(xl, xu) - # plt.ylim(yl, yu) - # plt.title("electron density") - # plt.figure() - # plt.imshow(te_samples, extent=[xl, xu, yl, yu], origin='lower') - # plt.colorbar() - # plt.xlim(xl, xu) - # plt.ylim(yl, yu) - # plt.title("electron temperature") - # - # def plot_species_density(self, species, ionisation): - # """ - # Make a plot of the requested species density in the SOLPS mesh plane. - # - # :param Element species: The species to plot. - # :param int ionisation: The charge state of the species to plot. - # """ - # - # species_dist = self.plasma.get_species(species, ionisation) - # - # me = self.mesh.mesh_extent - # xl, xu = (me['minr'], me['maxr']) - # yl, yu = (me['minz'], me['maxz']) - # species_samples = np.zeros((500, 500)) - # - # xrange = np.linspace(xl, xu, 500) - # yrange = np.linspace(yl, yu, 500) - # - # for i, x in enumerate(xrange): - # for j, y in enumerate(yrange): - # species_samples[j, i] = species_dist.distribution.density(x, 0.0, y) - # - # plt.figure() - # plt.imshow(species_samples, extent=[xl, xu, yl, yu], origin='lower') - # plt.colorbar() - # plt.xlim(xl, xu) - # plt.ylim(yl, yu) - # plt.title("Species {} - stage {} - density".format(species.name, ionisation)) - # - # def plot_pec_emission_lines(self, emission_lines, title="", vmin=None, vmax=None, log=False): - # """ - # Make a plot of the given PEC emission lines - # - # :param list emission_lines: List of PEC emission lines. - # :param str title: The title of the plot. - # :param float vmin: The minimum value for clipping the plots (default=None). - # :param float vmax: The maximum value for clipping the plots (default=None). - # :param bool log: Toggle a log plot for the data (default=False). - # """ - # - # me = self.mesh.mesh_extent - # xl, xu = (me['minr'], me['maxr']) - # yl, yu = (me['minz'], me['maxz']) - # emission_samples = np.zeros((500, 500)) - # - # xrange = np.linspace(xl, xu, 500) - # yrange = np.linspace(yl, yu, 500) - # - # for i, x in enumerate(xrange): - # for j, y in enumerate(yrange): - # for emitter in emission_lines: - # emission_samples[j, i] += emitter.emission(Point3D(x, 0.0, y), Vector3D(0, 0, 0), Spectrum(350, 700, 800)).total() - # - # if log: - # emission_samples = np.log(emission_samples) - # plt.figure() - # plt.imshow(emission_samples, extent=[xl, xu, yl, yu], origin='lower', vmin=vmin, vmax=vmax) - # plt.colorbar() - # plt.xlim(xl, xu) - # plt.ylim(yl, yu) - # plt.title(title) - # - # def plot_radiated_power(self): - # """ - # Make a plot of the given PEC emission lines - # - # :param list emission_lines: List of PEC emission lines. - # :param str title: The title of the plot. - # """ - # - # mesh = self.mesh - # me = mesh.mesh_extent - # total_rad = self.total_radiation - # - # xl, xu = (me['minr'], me['maxr']) - # yl, yu = (me['minz'], me['maxz']) - # - # # tri_index_lookup = mesh.triangle_index_lookup - # - # emission_samples = np.zeros((500, 500)) - # - # xrange = np.linspace(xl, xu, 500) - # yrange = np.linspace(yl, yu, 500) - # - # for i, x in enumerate(xrange): - # for j, y in enumerate(yrange): - # - # try: - # # k, l = mesh.triangle_to_grid_map[int(tri_index_lookup(x, y))] - # # emission_samples[i, j] = total_rad[k, l] - # emission_samples[j, i] = total_rad(x, 0, y) - # - # except ValueError: - # continue - # - # plt.figure() - # plt.imshow(emission_samples, extent=[xl, xu, yl, yu], origin='lower') - # plt.colorbar() - # plt.xlim(xl, xu) - # plt.ylim(yl, yu) - # plt.title("Radiated Power (W/m^3)") - def create_plasma(self, parent=None, transform=None, name=None): """ Make a CHERAB plasma object from this SOLPS simulation. @@ -947,6 +658,16 @@ def create_plasma(self, parent=None, transform=None, name=None): :rtype: Plasma """ + # Checking if the minimal required data is available to create a plasma object + if self.electron_density_f3d is None: + raise RuntimeError("Unable to create plasma object: electron density is not set.") + if self.electron_temperature_f3d is None: + raise RuntimeError("Unable to create plasma object: electron temperature is not set.") + if self.species_density_f3d is None: + raise RuntimeError("Unable to create plasma object: species density is not set.") + if self.ion_temperature_f3d is None: + raise RuntimeError("Unable to create plasma object: ion temperature is not set.") + mesh = self.mesh name = name or "SOLPS Plasma" plasma = Plasma(parent=parent, transform=transform, name=name) @@ -955,19 +676,19 @@ def create_plasma(self, parent=None, transform=None, name=None): plasma.geometry = Cylinder(radius, height) plasma.geometry_transform = translate(0, 0, mesh.mesh_extent['minz']) - try: - plasma.b_field = self.b_field_cartesian - except RuntimeError: + if self.b_field_cartesian is None: print('Warning! No magnetic field data available for this simulation.') + else: + plasma.b_field = self.b_field_cartesian # Create electron species if self.electron_velocities_cartesian is None: print('Warning! No electron velocity data available for this simulation.') - electron_velocity = lambda x, y, z: Vector3D(0, 0, 0) + electron_velocity = ConstantVector3D(Vector3D(0, 0, 0)) else: electron_velocity = self.electron_velocities_cartesian plasma.electron_distribution = Maxwellian(self.electron_density_f3d, self.electron_temperature_f3d, electron_velocity, electron_mass) - + if self.velocities_cartesian is None: print('Warning! No species velocities data available for this simulation.') @@ -988,7 +709,7 @@ def create_plasma(self, parent=None, transform=None, name=None): if self.velocities_cartesian is not None: velocity = self.velocities_cartesian[k] else: - velocity = lambda x, y, z: Vector3D(0, 0, 0) + velocity = ConstantVector3D(Vector3D(0, 0, 0)) if charge or self.neutral_temperature is None: # ions or neutral atoms (neutral temperature is not available) distribution = Maxwellian(self.species_density_f3d[k], self.ion_temperature_f3d, velocity, @@ -1006,7 +727,7 @@ def create_plasma(self, parent=None, transform=None, name=None): def _check_shape(name, value, shape): if value.shape != shape: - raise ValueError('Shape of "%s": %s mismatch the shape of SOLPS grid: %s.' % (name, value.shape, shape)) + raise ValueError('Shape of "{0}": {1} mismatch the shape of SOLPS grid: {2}.'.format(name, value.shape, shape)) def prefer_element(isotope): @@ -1018,3 +739,153 @@ def prefer_element(isotope): return isotope.element return isotope + + +def b2_flux_to_velocity(sim, poloidal_flux, radial_flux, parallel_velocity): + """ + Calculates velocities of all species at cell centres using B2 particle fluxes defined + at cell faces. + + :param SOLPSSimulation sim: SOLPSSimulation instance. + :param ndarray poloidal_flux: Poloidal flux of species in s-1. Must be a 3 dimensional array of + shape (num_species, mesh.ny, mesh.nx). + :param ndarray radial_flux: Radial flux of species in s-1. Must be a 3 dimensional array of + shape (num_species, mesh.ny, mesh.nx). + :param ndarray parallel_velocity: Parallel velocity of species in m/s. Must be a 3 dimensional + array of shape (num_species, mesh.ny, mesh.nx). + Parallel velocity is a velocity projection on magnetic + field direction. + + :return ndarray: Velocities of all species in cylindrical coordinates. + Array of shape (num_species, 3, mesh.ny, mesh.nx). + """ + if sim.b_field_cylindrical is None: + raise RuntimeError('Attribute "b_field_cylindrical" is not set.') + if sim.species_density is None: + raise RuntimeError('Attribute "species_density" is not set.') + + mesh = sim.mesh + b = sim.b_field_cylindrical + + poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) + radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) + parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) + + nx = mesh.nx # poloidal + ny = mesh.ny # radial + ns = len(sim.species_list) # number of species + + _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_shape('radial_flux', radial_flux, (ns, ny, nx)) + _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) + + poloidal_area = mesh.poloidal_area + radial_area = mesh.radial_area + leftix = mesh.neighbix[0] # poloidal prev. + leftiy = mesh.neighbiy[0] + bottomix = mesh.neighbix[1] # radial prev. + bottomiy = mesh.neighbiy[1] + rightix = mesh.neighbix[2] # poloidal next. + rightiy = mesh.neighbiy[2] + topix = mesh.neighbix[3] # radial next. + topiy = mesh.neighbiy[3] + + # Converting s-1 --> m-2 s-1 + poloidal_flux = np.divide(poloidal_flux, poloidal_area, out=np.zeros_like(poloidal_flux), where=poloidal_area > 0) + radial_flux = np.divide(radial_flux, radial_area, out=np.zeros_like(radial_flux), where=radial_area > 0) + + # Obtaining left velocity + dens_neighb = sim.species_density[:, leftiy, leftix] # density in the left neighbouring cell + has_neighbour = ((leftix > -1) * (leftiy > -1)) # check if has left neighbour + neg_flux = (poloidal_flux < 0) * (sim.species_density > 0) # will use density in this cell if flux is negative + pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour # will use density in neighbouring cell if flux is positive + velocity_left = np.divide(poloidal_flux, sim.species_density, out=np.zeros((ns, ny, nx)), where=neg_flux) + velocity_left = np.divide(poloidal_flux, dens_neighb, out=velocity_left, where=pos_flux) + poloidal_face_normal = mesh.radial_basis_vector[[1, 0]] + poloidal_face_normal[1] *= -1 + velocity_left = velocity_left[:, None] * poloidal_face_normal # to vector in RZ + + # Obtaining bottom velocity + dens_neighb = sim.species_density[:, bottomiy, bottomix] + has_neighbour = ((bottomix > -1) * (bottomiy > -1)) + neg_flux = (radial_flux < 0) * (sim.species_density > 0) + pos_flux = (poloidal_flux > 0) * (dens_neighb > 0) * has_neighbour + velocity_bottom = np.divide(radial_flux, sim.species_density, out=np.zeros((ns, ny, nx)), where=neg_flux) + velocity_bottom = np.divide(radial_flux, dens_neighb, out=velocity_bottom, where=pos_flux) + radial_face_normal = mesh.poloidal_basis_vector[[1, 0]] + radial_face_normal[0] *= -1 + velocity_bottom = velocity_bottom[:, None] * radial_face_normal # to RZ + + # Obtaining right and top velocities + velocity_right = velocity_left[:, :, rightiy, rightix] + velocity_right[:, :, (rightix < 0) + (rightiy < 0)] = 0 + + velocity_top = velocity_bottom[:, :, topiy, topix] + velocity_top[:, :, (topix < 0) + (topiy < 0)] = 0 + + vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates + + # Projection of velocity on RZ-plane + vcyl[:, [0, 2]] = 0.25 * (velocity_bottom + velocity_left + velocity_top + velocity_right) + + # Obtaining toroidal velocity + bmagn = np.sqrt((b * b).sum(0)) + vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[0] - vcyl[:, 2] * b[2]) / b[1] + + return vcyl + + +def eirene_flux_to_velocity(sim, poloidal_flux, radial_flux, parallel_velocity): + """ + Calculates velocities of neutral atoms using Eirene particle fluxes defined at cell centre. + + :param SOLPSSimulation sim: SOLPSSimulation instance. + :param ndarray poloidal_flux: Poloidal flux of atoms in m-2 s-1. Must be a 3 dimensional array of + shape (num_atoms, mesh.ny, mesh.nx). + :param ndarray radial_flux: Radial flux of atoms in m-2 s-1. Must be a 3 dimensional array of + shape (num_atoms, mesh.ny, mesh.nx). + :param ndarray parallel_velocity: Parallel velocity of atoms in m/s. Must be a 3 dimensional + array of shape (num_atoms, mesh.ny, mesh.nx). + Parallel velocity is a velocity projection on magnetic + field direction. + + :return ndarray: Velocities of neutral atoms in cylindrical coordinates. + Array of shape (num_atoms, 3, mesh.ny, mesh.nx). + """ + if sim.b_field_cylindrical is None: + raise RuntimeError('Attribute "b_field_cylindrical" is not set.') + if sim.species_density is None: + raise RuntimeError('Attribute "species_density" is not set.') + + mesh = sim.mesh + b = sim.b_field_cylindrical + + poloidal_flux = np.array(poloidal_flux, dtype=np.float64, copy=False) + radial_flux = np.array(radial_flux, dtype=np.float64, copy=False) + parallel_velocity = np.array(parallel_velocity, dtype=np.float64, copy=False) + + nx = mesh.nx # poloidal + ny = mesh.ny # radial + ns = len(sim.neutral_list) # number of neutral atoms + + _check_shape('poloidal_flux', poloidal_flux, (ns, ny, nx)) + _check_shape('radial_flux', radial_flux, (ns, ny, nx)) + _check_shape('parallel_velocity', parallel_velocity, (ns, ny, nx)) + + neutral_indx = [k for k, sp in enumerate(sim.species_list) if sp[1] == 0] + density = sim.species_density[neutral_indx, :] + + # Obtaining velocity + poloidal_velocity = np.divide(poloidal_flux, density, out=np.zeros_like(density), where=(density > 0)) + radial_velocity = np.divide(radial_flux, density, out=np.zeros_like(density), where=(density > 0)) + + vcyl = np.zeros((ns, 3, ny, nx)) # velocities in cylindrical coordinates + + # Projection of velocity on RZ-plane + vcyl[:, [0, 2]] = (poloidal_velocity[:, None] * mesh.poloidal_basis_vector + radial_velocity[:, None] * mesh.radial_basis_vector) + + # Obtaining toroidal velocity + bmagn = np.sqrt((b * b).sum(0)) + vcyl[:, 1] = (parallel_velocity * bmagn - vcyl[:, 0] * b[0] - vcyl[:, 2] * b[2]) / b[1] + + return vcyl