modified: anasen_fem/clean.sh new file: anasen_fem/dotproduct.py new file: anasen_fem/paraview_dotproduct.py modified: anasen_fem/paraview_plotter.py modified: anasen_fem/run.py modified: anasen_fem/scalars.dat.names new file: anasen_fem/scalars_weight.dat new file: anasen_fem/scalars_weight.dat.names modified: anasen_fem/wires2d.sif new file: anasen_fem/wires2d_weight.sif modified: anasen_fem/wires_gmsh2d_bc.py
280 lines
12 KiB
Python
Executable File
280 lines
12 KiB
Python
Executable File
#!/home/vsitaraman/ParaView-6.1.0-RC1-MPI-Linux-Python3.12-x86_64/bin/pvbatch
|
|
# Point the shebang above at your own pvbatch/pvpython, as for paraview_plotter.py.
|
|
#
|
|
# Renders the E . E_w map written by dotproduct.py.
|
|
#
|
|
# By Shockley-Ramo the dot product describes the current induced on ONE
|
|
# electrode: the cathode that wires2d_weight.sif held at 1 V. Everywhere else
|
|
# the number is just the tail of that electrode's weighting field, so the view
|
|
# is centred on that wire and nothing else. Its position is read out of the
|
|
# weighting solve rather than recomputed from the geometry -- the driven
|
|
# electrode is wherever the weighting potential reaches 1.
|
|
#
|
|
# Usage (from anasen_fem/):
|
|
# ./paraview_dotproduct.py [dotproduct.vtu] [output.png] [weight.vtu]
|
|
#
|
|
# Re-plotting an archived z needs its archived weighting solve as well, since
|
|
# that is where the driven cathode's position is read from:
|
|
# ./paraview_dotproduct.py wires2d/vtu_files/dotproduct_10_0.0000.vtu \
|
|
# png/DotProduct_output_z_10_0.0000.png \
|
|
# wires2d/vtu_files/elfield_weight_10_0.0000.vtu
|
|
|
|
import sys
|
|
|
|
import numpy as np
|
|
from paraview.simple import *
|
|
|
|
INPUT_VTU = sys.argv[1] if len(sys.argv) > 1 else "wires2d/dotproduct.vtu"
|
|
OUTPUT_IMAGE = sys.argv[2] if len(sys.argv) > 2 else "DotProduct_output.png"
|
|
|
|
# Weighting solve, used only to locate the driven cathode. Must be the one that
|
|
# belongs to this dot product -- the wire moves with z.
|
|
WEIGHT_VTU = sys.argv[3] if len(sys.argv) > 3 else "wires2d/elfield_weight_t0001.vtu"
|
|
|
|
# Half-height of the view in metres. Cathodes sit every 15 degrees at a radius
|
|
# of about 36.4 mm, so the neighbours either side are 9.5 mm away; 14 mm takes
|
|
# them both in with a margin.
|
|
# Image size. The structure round the driven cathode is much wider than it is
|
|
# tall, so a square frame spends most of its area on empty gas.
|
|
VIEW_SIZE = [2000, 1200]
|
|
|
|
# CameraParallelScale, which is the half-HEIGHT of the view in metres. The
|
|
# width follows from the aspect: half-height * (VIEW_SIZE[0] / VIEW_SIZE[1]).
|
|
# So widening the frame alone does not crop anything, it just shows more
|
|
# sideways; the scale has to come down with it. 0.0084 at 2000x1200 keeps the
|
|
# same 28 mm width as the old square 0.014 and trims the height to 16.8 mm.
|
|
ZOOM_SCALE = 0.0084
|
|
|
|
# The driven cathode swings round the axis as z changes, so with a fixed "up"
|
|
# every z comes out rotated by a different amount and the shots cannot be laid
|
|
# side by side. Aligning up with the outward radial direction through the
|
|
# cathode puts the detector axis below and the barrel wall above in every
|
|
# frame, whatever z. Set False for plain screen-aligned axes.
|
|
ALIGN_RADIAL = True
|
|
|
|
# Shift of the view centre along the outward radial, in metres. Up in the frame
|
|
# is the outward radial, so a NEGATIVE value walks the camera in towards the
|
|
# detector axis and the cathode rides higher in the picture. Positive drops it.
|
|
# Purely framing; it does not move the wire.
|
|
CENTER_SHIFT = -0.0036
|
|
|
|
# Second, wider shot over the same quadrant paraview_plotter.py frames, so the
|
|
# dot product can be laid beside the field and contour views of that z. Centre
|
|
# is at x < 0, y > 0; negate the y for the lower-left quadrant instead.
|
|
|
|
# Colour range for DotProduct. Both ends negative: E and E_w point in broadly
|
|
# opposite senses over most of the drift volume.
|
|
# Most of the painted patch lies between -2e7 and -2e6. Against a range
|
|
# reaching -3.1e8 that is the top few percent of the scale, so it all came out
|
|
# at the white end of Greens. Narrowing the range spreads those values over the
|
|
# green; anything stronger simply clamps to the darkest end, which only affects
|
|
# the last fraction of a millimetre at the wire surface.
|
|
DOT_MIN, DOT_MAX = -8.0e7, -3.0e6
|
|
|
|
# Opacity of the painted map. Anything at least as strong as OPAQUE_BELOW is
|
|
# drawn solid; from there it ramps down to transparent at FADE_TO, so the grey
|
|
# shows through with a soft round edge rather than the ragged one a hard cut
|
|
# leaves.
|
|
#
|
|
# The plateau matters more than either endpoint. Ramping all the way from
|
|
# DOT_MIN (-3.1e8) meant the opacity at -5e7 was only about 0.13, so the whole
|
|
# patch read as washed out however the fade point was set.
|
|
OPAQUE_BELOW = -2.0e7
|
|
FADE_TO = -2.0e6
|
|
|
|
# Coarse cut, only to keep the far field out of the render. Must be nearer zero
|
|
# than FADE_TO so its hard edge lands where the surface is already fully
|
|
# transparent and cannot be seen.
|
|
PAINT_CUT = -5.0e5
|
|
|
|
# "signed-log" maps sign and magnitude together through a signed logarithm and
|
|
# a diverging colour map; "linear" is the earlier one-sided Greens scale, which
|
|
# the constants above still drive.
|
|
COLOR_MODE = "signed-log"
|
|
# X0 sets where the mapping turns from linear to logarithmic, and so how much
|
|
# of the weak far field survives at all: at 1e6 the data map to -2.51 .. 1.62,
|
|
# against -3.51 .. 2.61 at 1e5. Raise it to paint less.
|
|
SYMLOG_X0 = 1.0e6
|
|
SYMLOG_LIMIT = 2.5 # symmetric colour limit; match it to the mapped range
|
|
SYMLOG_OPAQUE = 0.6 # |value| at which the map is fully opaque
|
|
SYMLOG_FADE = 0.3 # |value| below which it is transparent
|
|
|
|
# Shape of the two opacity ramps, as opposed to where they sit.
|
|
#
|
|
# A piecewise function's midpoint describes the segment running RIGHTWARD from
|
|
# each node, and the two ramps run in opposite senses -- falling on the
|
|
# negative side, rising on the positive. So a single midpoint below 0.5 applied
|
|
# to both makes the negative lobe fade out sooner while the positive lobe goes
|
|
# opaque sooner, rendering the two polarities at different effective
|
|
# thresholds. Mirroring it keeps them symmetric: below 0.5 both sides go opaque
|
|
# sooner, so more is painted, equally on each.
|
|
#
|
|
# Sharpness is the independent dial for how abrupt the edge is: 0 smooth, 1 a
|
|
# hard step. 0.5 is already close to a step.
|
|
SYMLOG_MIDPOINT = 0.3
|
|
SYMLOG_SHARPNESS = 0.0
|
|
DIVERGING_PRESET = "PRGn" # purple / white / green; zero sits at white
|
|
|
|
BACKGROUND = [0.35, 0.34, 0.33] # grey
|
|
|
|
# Equipotential lines over the whole frame, painted gas or not. Off by default:
|
|
# the map is the subject here, and the lines are already in the contour plots.
|
|
SHOW_CONTOURS = False
|
|
CONTOUR_BY = "potential"
|
|
CONTOUR_LEVELS = list(np.arange(0, 660, 660 / 40.0))
|
|
|
|
|
|
def threshold_between(source, array, low, high):
|
|
"""Threshold filter, tolerating the pre-5.10 property names."""
|
|
t = Threshold(Input=source, Scalars=["POINTS", array])
|
|
try:
|
|
t.LowerThreshold = low
|
|
t.UpperThreshold = high
|
|
t.ThresholdMethod = "Between"
|
|
except Exception:
|
|
t.ThresholdRange = [low, high]
|
|
return t
|
|
|
|
|
|
# --- locate the driven cathode: the weighting potential is 1 on it ---
|
|
weight_reader = XMLUnstructuredGridReader(FileName=[WEIGHT_VTU])
|
|
driven = threshold_between(weight_reader, "potential", 0.99, 1.01)
|
|
driven.UpdatePipeline()
|
|
|
|
info = driven.GetDataInformation()
|
|
if info.GetNumberOfPoints() == 0:
|
|
sys.exit(
|
|
"no nodes with weighting potential ~1 in %s -- the weighting solve did\n"
|
|
"not drive any electrode, so there is nothing to centre on." % WEIGHT_VTU
|
|
)
|
|
b = info.GetBounds()
|
|
CATHODE = [0.5 * (b[0] + b[1]), 0.5 * (b[2] + b[3]), 0.0]
|
|
print("driven cathode at x = %.5f m, y = %.5f m" % (CATHODE[0], CATHODE[1]))
|
|
|
|
radius = (CATHODE[0] ** 2 + CATHODE[1] ** 2) ** 0.5
|
|
outward = [CATHODE[0] / radius, CATHODE[1] / radius, 0.0] if radius > 0 else [0.0, 1.0, 0.0]
|
|
|
|
# Frame centre, offset from the wire along that radial.
|
|
CENTER = [
|
|
CATHODE[0] + CENTER_SHIFT * outward[0],
|
|
CATHODE[1] + CENTER_SHIFT * outward[1],
|
|
0.0,
|
|
]
|
|
|
|
Delete(driven)
|
|
Delete(weight_reader)
|
|
|
|
# --- the dot product map ---
|
|
reader = XMLUnstructuredGridReader(FileName=[INPUT_VTU])
|
|
|
|
renderView = GetActiveViewOrCreate("RenderView")
|
|
renderView.ViewSize = VIEW_SIZE
|
|
renderView.OrientationAxesVisibility = 1
|
|
renderView.UseColorPaletteForBackground = 0
|
|
renderView.Background = BACKGROUND
|
|
renderView.MultiSamples = 8
|
|
|
|
if COLOR_MODE == "signed-log":
|
|
# E . E_w spans about 3.5 decades negative and 2.6 positive, and 38% of the
|
|
# nodes are positive -- charge drifting there induces current of the
|
|
# opposite polarity on the cathode. A linear scale over one sign shows
|
|
# neither the decades nor the sign, so map through a signed log:
|
|
#
|
|
# sign(d) * log10(1 + |d| / SYMLOG_X0)
|
|
#
|
|
# sign() is not in the calculator namespace, hence d/(|d|+1); the +1 is
|
|
# negligible beside the 1e5 scale and the expression tends to 0 anyway
|
|
# where d does.
|
|
calc = PythonCalculator(Input=reader)
|
|
calc.ArrayName = "SignedLogDot"
|
|
calc.ArrayAssociation = "Point Data"
|
|
calc.Expression = (
|
|
"(DotProduct/(abs(DotProduct)+1.0))*log10(1.0+abs(DotProduct)/%g)"
|
|
% SYMLOG_X0
|
|
)
|
|
painted = calc
|
|
array = "SignedLogDot"
|
|
colour_range = (-SYMLOG_LIMIT, SYMLOG_LIMIT) # symmetric, so zero is neutral
|
|
preset = DIVERGING_PRESET
|
|
# Transparent either side of zero, opaque past SYMLOG_OPAQUE, both signs.
|
|
_m, _s = SYMLOG_MIDPOINT, SYMLOG_SHARPNESS
|
|
opacity_points = [
|
|
-SYMLOG_LIMIT, 1.0, 0.5, _s,
|
|
-SYMLOG_OPAQUE, 1.0, 1.0 - _m, _s, # mirrored: this ramp falls
|
|
-SYMLOG_FADE, 0.0, 0.5, _s,
|
|
SYMLOG_FADE, 0.0, _m, _s, # this one rises
|
|
SYMLOG_OPAQUE, 1.0, 0.5, _s,
|
|
SYMLOG_LIMIT, 1.0, 0.5, _s,
|
|
]
|
|
bar_title = "sign(E.Ew) log10(1+|E.Ew|/%g)" % SYMLOG_X0
|
|
else:
|
|
painted = threshold_between(reader, "DotProduct", -1.0e30, PAINT_CUT)
|
|
array = "DotProduct"
|
|
colour_range = (DOT_MIN, DOT_MAX)
|
|
preset = "Greens"
|
|
opacity_points = [
|
|
DOT_MIN, 1.0, 0.5, 0.0,
|
|
OPAQUE_BELOW, 1.0, 0.5, 0.0,
|
|
FADE_TO, 0.0, 0.5, 0.0,
|
|
]
|
|
bar_title = "E . E_w (V/m^2)"
|
|
|
|
surface_display = Show(painted, renderView)
|
|
surface_display.Representation = "Surface"
|
|
ColorBy(surface_display, ("POINTS", array))
|
|
|
|
dotLUT = GetColorTransferFunction(array)
|
|
dotLUT.ApplyPreset(preset, True)
|
|
dotLUT.RescaleTransferFunction(colour_range[0], colour_range[1])
|
|
|
|
# Points go (value, opacity, midpoint, sharpness) in ascending value.
|
|
dotPWF = GetOpacityTransferFunction(array)
|
|
dotPWF.Points = opacity_points
|
|
dotLUT.ScalarOpacityFunction = dotPWF
|
|
dotLUT.EnableOpacityMapping = 1
|
|
|
|
surface_display.SetScalarBarVisibility(renderView, True)
|
|
try:
|
|
GetScalarBar(dotLUT, renderView).Title = bar_title
|
|
except Exception as exc:
|
|
print("note: could not set the scalar bar title (%s)" % exc)
|
|
|
|
# --- contour lines over the whole field, including the unpainted gas ---
|
|
if SHOW_CONTOURS:
|
|
contour_filter = Contour(Input=reader, ContourBy=["POINTS", CONTOUR_BY])
|
|
contour_filter.Isosurfaces = CONTOUR_LEVELS
|
|
|
|
contour_display = Show(contour_filter, renderView)
|
|
contour_display.LineWidth = 1.0
|
|
contour_display.DiffuseColor = [1.0, 1.0, 1.0]
|
|
# ColorBy(rep, None) is rejected by ParaView 6.1 -- it forwards the
|
|
# association as the string 'NONE' and raises -- so set the property.
|
|
contour_display.ColorArrayName = [None, ""]
|
|
contour_display.SetScalarBarVisibility(renderView, False)
|
|
|
|
# --- camera: parallel projection, centred on the driven cathode ---
|
|
# ParaView resets the camera on a view's FIRST render, throwing away anything
|
|
# set beforehand, so burn that render here and set the camera after it.
|
|
# Measured: scale 0.008 set before the first Render comes back as 0.021; set
|
|
# again afterwards it holds.
|
|
Render()
|
|
|
|
view = GetActiveView()
|
|
view.CameraParallelScale = ZOOM_SCALE
|
|
view.CenterOfRotation = CENTER
|
|
view.CameraPosition = [CENTER[0], CENTER[1], 1.0]
|
|
view.CameraFocalPoint = CENTER
|
|
if ALIGN_RADIAL:
|
|
up = outward
|
|
print("view rotated %.1f deg so the outward radial is up"
|
|
% np.degrees(np.arctan2(up[0], up[1])))
|
|
else:
|
|
up = [0.0, 1.0, 0.0]
|
|
view.CameraViewUp = up
|
|
view.CameraParallelProjection = 1
|
|
|
|
Render()
|
|
SaveScreenshot(OUTPUT_IMAGE)
|
|
print("Saved %s" % OUTPUT_IMAGE)
|
|
|