From 73d5e9967239dd2b817071f286041fdb16021f6c Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 18 Apr 2024 21:07:07 +0200 Subject: [PATCH 001/171] Add: show_swept-file and swept-function in create-file --- examples/show_swept.py | 69 +++++++++++++++++++++++++++++++++++++++ splinepy/helpme/create.py | 36 ++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 examples/show_swept.py diff --git a/examples/show_swept.py b/examples/show_swept.py new file mode 100644 index 000000000..5236c45fc --- /dev/null +++ b/examples/show_swept.py @@ -0,0 +1,69 @@ +import gustaf as gus +import numpy as np + +import splinepy + +if __name__ == "__main__": + # trajectory + # define degrees + ds1 = [2] + # define knot vectors + kvs1 = [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]] + # define control points + cps1 = np.array( + [ + [0.0, 0.0, 0.0], + [3.0, 1.0, 0.0], + [4.0, 0.0, 0.0], + ] + ) + # init trajectory as bspline + trajectory = splinepy.BSpline( + degrees=ds1, + knot_vectors=kvs1, + control_points=cps1, + ) + # show + # trajectory.show() + + # cross section + # define degrees + ds1 = [1] + + # define knot vectors + kvs1 = [[0.0, 0.0, 1.0, 1.0]] + + # define control points + cps1 = np.array( + [ + [0.0, 0.0, -1.0], + [0.0, 0.0, 1.0], + ] + ) + + # init cross section as bspline + cross_section = splinepy.BSpline( + degrees=ds1, + knot_vectors=kvs1, + control_points=cps1, + ) + + def der(data, on): + return data.derivative(on, [1]) + + trajectory.spline_data["der"] = splinepy.SplineDataAdaptor( + trajectory, function=der + ) + trajectory.show_options["arrow_data"] = "der" + trajectory.show_options["arrow_data_on"] = np.linspace(0, 1, 20).reshape( + -1, 1 + ) + + derivative = trajectory.derivative([[0.5], [1]], [1]) + print(derivative) + + gus.show( + ["Trajectory", trajectory], + ["Cross section", cross_section], + resolution=50, + ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 1e3d4ee3d..bc319ce53 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -298,6 +298,42 @@ def revolved( return type(spline)(**spline_dict) +def swept(cross_section, trajectory, nsections): + """Sweeps a cross-section along a trajectory + + Parameters + ---------- + crossection : Spline + Cross-section to be swept + trajectory : Spline + Trajectory along which the cross-section is swept + nsections : int + Number of sections trajectory is divided into + + Returns + ------- + swept_spline : Spline + Spline resulting from the sweep + """ + + from splinepy.spline import Spline as _Spline + + # Check input type + if not isinstance(cross_section, _Spline): + raise NotImplementedError("Sweeps only works for splines") + if not isinstance(trajectory, _Spline): + raise NotImplementedError("Sweeps only works for splines") + + # Check if trajectory is a curve + if trajectory.dim != 1: + raise ValueError("Trajectory must be a curve") + + # dummy variable for nsections + _ = nsections + + # IMPLEMENTATION OF SWEEPING SURFACE + + def from_bounds(parametric_bounds, physical_bounds): """Creates a minimal spline with given parametric bounds, physical bounds. Physical bounds can have less or equal number of From 0b5315a46fb7061a4887cf7973b4ced76939b94c Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 29 Apr 2024 17:21:59 +0200 Subject: [PATCH 002/171] Add: first kinda swept surface implementation in show_swept-file --- examples/show_swept.py | 60 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 5236c45fc..c411fad27 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -23,18 +23,27 @@ knot_vectors=kvs1, control_points=cps1, ) - # show - # trajectory.show() + # define sections along trajectory + nsect = len(kvs1[0])-ds1[0]-1 + + # compute parameter values for inserting cross sections + par_value = np.zeros((nsect,1)) + for i in range(nsect): + par_value[i][0] = (kvs1[0][i+1]+kvs1[0][i+2])/2 + + # evaluate trajectory at these parameter values + evals = trajectory.evaluate(par_value) + # cross section # define degrees - ds1 = [1] + ds_cs = [1] # define knot vectors - kvs1 = [[0.0, 0.0, 1.0, 1.0]] + kvs_cs = [[0.0, 0.0, 1.0, 1.0]] # define control points - cps1 = np.array( + cps_cs = np.array( [ [0.0, 0.0, -1.0], [0.0, 0.0, 1.0], @@ -43,11 +52,38 @@ # init cross section as bspline cross_section = splinepy.BSpline( - degrees=ds1, - knot_vectors=kvs1, - control_points=cps1, + degrees=ds_cs, + knot_vectors=kvs_cs, + control_points=cps_cs, + ) + + # set cross section CPs along trajectory + cps_eval = [] + for eval_point in evals: + # for-loop representing the nr. of cross section CPs + # note: only working for this special case + for i in range(len(cps_cs)): + current_cps = [] + current_cps.append(eval_point[0]) + current_cps.append(eval_point[1]) + current_cps.append(cps_cs[i][2]) + cps_eval.append(current_cps) + + fitting_points = np.array(cps_eval) + + # fit surface + interpolated_surface, _ = splinepy.helpme.fit.surface( + fitting_points=fitting_points, + size=[2, 3], + n_control_points=[2, 3], + degrees=[1, 2], ) + + # testing derivative and evaluation + # derivative = trajectory.derivative([[0.5], [1]], [1]) + # evals = trajectory.evaluate([[0.5]]) + # show def der(data, on): return data.derivative(on, [1]) @@ -59,11 +95,13 @@ def der(data, on): -1, 1 ) - derivative = trajectory.derivative([[0.5], [1]], [1]) - print(derivative) - gus.show( ["Trajectory", trajectory], ["Cross section", cross_section], resolution=50, ) + + gus.show( + ["Surface", interpolated_surface], + resolution=50, + ) From 630ca7b8a80a2d690d7d3f3bade22d982d340e47 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 2 May 2024 14:12:41 +0200 Subject: [PATCH 003/171] Fix: Moved computation from show_swept-file to create-file --- examples/show_swept.py | 32 ++++-------------------------- splinepy/helpme/create.py | 41 ++++++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index c411fad27..d1246eafe 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -27,14 +27,6 @@ # define sections along trajectory nsect = len(kvs1[0])-ds1[0]-1 - # compute parameter values for inserting cross sections - par_value = np.zeros((nsect,1)) - for i in range(nsect): - par_value[i][0] = (kvs1[0][i+1]+kvs1[0][i+2])/2 - - # evaluate trajectory at these parameter values - evals = trajectory.evaluate(par_value) - # cross section # define degrees ds_cs = [1] @@ -57,26 +49,10 @@ control_points=cps_cs, ) - # set cross section CPs along trajectory - cps_eval = [] - for eval_point in evals: - # for-loop representing the nr. of cross section CPs - # note: only working for this special case - for i in range(len(cps_cs)): - current_cps = [] - current_cps.append(eval_point[0]) - current_cps.append(eval_point[1]) - current_cps.append(cps_cs[i][2]) - cps_eval.append(current_cps) - - fitting_points = np.array(cps_eval) - - # fit surface - interpolated_surface, _ = splinepy.helpme.fit.surface( - fitting_points=fitting_points, - size=[2, 3], - n_control_points=[2, 3], - degrees=[1, 2], + interpolated_surface = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section, + nsections=nsect, ) # testing derivative and evaluation diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index bc319ce53..cec1be6bb 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -6,7 +6,7 @@ from splinepy import settings as _settings from splinepy.utils import log as _log from splinepy.utils.data import make_matrix as _make_matrix - +from splinepy.helpme.fit import surface def embedded(spline, new_dimension): """Embeds Spline in given dimension. @@ -324,15 +324,38 @@ def swept(cross_section, trajectory, nsections): if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweeps only works for splines") - # Check if trajectory is a curve - if trajectory.dim != 1: - raise ValueError("Trajectory must be a curve") - - # dummy variable for nsections - _ = nsections - - # IMPLEMENTATION OF SWEEPING SURFACE + # compute parameter values for inserting cross sections + par_value = _np.zeros((nsections,1)) + for i in range(nsections): + par_value[i][0] = (trajectory.knot_vectors[0][i+1]+trajectory.knot_vectors[0][i+2])/2 + + # evaluate trajectory at these parameter values + evals = trajectory.evaluate(par_value) + + # set cross section CPs along trajectory + cps_eval = [] + for eval_point in evals: + # for-loop representing the nr. of cross section CPs + # note: only working for this special case + for i in range(len(cross_section.control_points)): + current_cps = [] + current_cps.append(eval_point[0]) + current_cps.append(eval_point[1]) + current_cps.append(cross_section.control_points[i][2]) + cps_eval.append(current_cps) + + fitting_points = _np.array(cps_eval) + + # fit surface + interpolated_surface, _ = surface( + fitting_points=fitting_points, + size=[2, 3], + n_control_points=[2, 3], + degrees=[1, 2], + ) + return interpolated_surface + def from_bounds(parametric_bounds, physical_bounds): """Creates a minimal spline with given parametric bounds, physical bounds. From afd3b8bd3ef5d0837c8f0402e94d8b43f5a57fb2 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 2 May 2024 14:51:15 +0200 Subject: [PATCH 004/171] Add: function for transformation matrix --- splinepy/helpme/create.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index cec1be6bb..7272e4c9d 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -324,6 +324,23 @@ def swept(cross_section, trajectory, nsections): if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweeps only works for splines") + # compute transformation matrix + # parameters: trajectory traj and parametric value v + def transformation_matrix(traj, v): + # origin of local coordinate system + o = traj.evaluate([[v]]) + # local directions in global coordinates + x = traj.derivative([[v]], [1]) / _np.linalg.norm(traj.derivative([[v]], [1]) ) + z = _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) / _np.linalg.norm(_np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2]))) + y = _np.cross(x,z) + # transformation matrix + A = [] + A = _np.vstack((x, y, z)) + return A + + A = transformation_matrix(trajectory, 0) + print(A) + # compute parameter values for inserting cross sections par_value = _np.zeros((nsections,1)) for i in range(nsections): From a789aea11bd611c8404b053656deccb5c6597278 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 2 May 2024 17:44:35 +0200 Subject: [PATCH 005/171] Add: show_options changed in show_swept-file --- examples/show_swept.py | 5 ++++- splinepy/helpme/create.py | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index d1246eafe..2f1cbfa07 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -54,7 +54,7 @@ cross_section=cross_section, nsections=nsect, ) - + # testing derivative and evaluation # derivative = trajectory.derivative([[0.5], [1]], [1]) # evals = trajectory.evaluate([[0.5]]) @@ -70,6 +70,9 @@ def der(data, on): trajectory.show_options["arrow_data_on"] = np.linspace(0, 1, 20).reshape( -1, 1 ) + trajectory.show_options["control_mesh"] = False + cross_section.show_options["control_mesh"] = False + interpolated_surface.show_options["control_mesh"] = False gus.show( ["Trajectory", trajectory], diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 7272e4c9d..89b34a97c 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -332,12 +332,14 @@ def transformation_matrix(traj, v): # local directions in global coordinates x = traj.derivative([[v]], [1]) / _np.linalg.norm(traj.derivative([[v]], [1]) ) z = _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) / _np.linalg.norm(_np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2]))) - y = _np.cross(x,z) - # transformation matrix - A = [] - A = _np.vstack((x, y, z)) + y = _np.cross(z,x) + # transformation matrix from global to local coordinates + T = [] + T = _np.vstack((x, y, z)) + # transformation matrix from local to global coordinates + A = _np.linalg.inv(T) return A - + A = transformation_matrix(trajectory, 0) print(A) @@ -349,7 +351,7 @@ def transformation_matrix(traj, v): # evaluate trajectory at these parameter values evals = trajectory.evaluate(par_value) - # set cross section CPs along trajectory + # set cross section evaluation points along trajectory cps_eval = [] for eval_point in evals: # for-loop representing the nr. of cross section CPs From 0120f9cc72f3c5c6586c775fd8582249c165e810 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 6 May 2024 12:45:54 +0200 Subject: [PATCH 006/171] Add: positioning of cross-section control points --- examples/show_swept.py | 12 ++++--- splinepy/helpme/create.py | 69 ++++++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 2f1cbfa07..d1f675a7c 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -25,20 +25,22 @@ ) # define sections along trajectory - nsect = len(kvs1[0])-ds1[0]-1 + nsect = len(kvs1[0]) - ds1[0] - 1 # cross section # define degrees - ds_cs = [1] + ds_cs = [2] # define knot vectors - kvs_cs = [[0.0, 0.0, 1.0, 1.0]] + kvs_cs = [[0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]] # define control points cps_cs = np.array( [ - [0.0, 0.0, -1.0], - [0.0, 0.0, 1.0], + [0.0, 0.0, 0.0], + [1.0, 1.0, 0.0], + [2.0, 0.0, 0.0], + [3.0, 1.0, 0.0], ] ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 89b34a97c..202df67e0 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -4,9 +4,10 @@ from gustaf.utils import arr as _arr from splinepy import settings as _settings +from splinepy.helpme.fit import surface from splinepy.utils import log as _log from splinepy.utils.data import make_matrix as _make_matrix -from splinepy.helpme.fit import surface + def embedded(spline, new_dimension): """Embeds Spline in given dimension. @@ -324,57 +325,73 @@ def swept(cross_section, trajectory, nsections): if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweeps only works for splines") - # compute transformation matrix + # compute transformation matrix # parameters: trajectory traj and parametric value v def transformation_matrix(traj, v): - # origin of local coordinate system - o = traj.evaluate([[v]]) # local directions in global coordinates - x = traj.derivative([[v]], [1]) / _np.linalg.norm(traj.derivative([[v]], [1]) ) - z = _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) / _np.linalg.norm(_np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2]))) - y = _np.cross(z,x) + x = traj.derivative([[v]], [1]) / _np.linalg.norm( + traj.derivative([[v]], [1]) + ) + z = _np.cross( + traj.derivative([[v]], [1]), traj.derivative([[v]], [2]) + ) / _np.linalg.norm( + _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) + ) + y = _np.cross(z, x) # transformation matrix from global to local coordinates T = [] T = _np.vstack((x, y, z)) # transformation matrix from local to global coordinates A = _np.linalg.inv(T) + A = _np.negative(A) return A - - A = transformation_matrix(trajectory, 0) - print(A) # compute parameter values for inserting cross sections - par_value = _np.zeros((nsections,1)) + par_value = _np.zeros((nsections, 1)) for i in range(nsections): - par_value[i][0] = (trajectory.knot_vectors[0][i+1]+trajectory.knot_vectors[0][i+2])/2 + par_value[i][0] = ( + trajectory.knot_vectors[0][i + 1] + + trajectory.knot_vectors[0][i + 2] + ) / 2 # evaluate trajectory at these parameter values evals = trajectory.evaluate(par_value) # set cross section evaluation points along trajectory cps_eval = [] - for eval_point in evals: - # for-loop representing the nr. of cross section CPs - # note: only working for this special case - for i in range(len(cross_section.control_points)): - current_cps = [] - current_cps.append(eval_point[0]) - current_cps.append(eval_point[1]) - current_cps.append(cross_section.control_points[i][2]) - cps_eval.append(current_cps) + for index, eval_point in enumerate(evals): + # evaluate transformation matrix for each trajectory point + A = transformation_matrix(trajectory, par_value[index][0]) + for cscp in cross_section.control_points: + # rotation matrix for 90 degrees around y-axis + R = _np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]]) + # rotate cross section in trajectory direction + normal_cscp = _np.dot(R, cscp) + # transform cross section to global coordinates + normal_cscp = _np.dot(A, normal_cscp) + # translate cross section to trajectory point + normal_cscp = normal_cscp + eval_point + # append control point to list + cps_eval.append(normal_cscp) fitting_points = _np.array(cps_eval) - + # fit surface interpolated_surface, _ = surface( fitting_points=fitting_points, - size=[2, 3], - n_control_points=[2, 3], - degrees=[1, 2], + size=[ + len(cross_section.control_points), + len(trajectory.control_points), + ], + n_control_points=[ + len(cross_section.control_points), + len(trajectory.control_points), + ], + degrees=[int(cross_section.degrees), int(trajectory.degrees)], ) return interpolated_surface - + def from_bounds(parametric_bounds, physical_bounds): """Creates a minimal spline with given parametric bounds, physical bounds. From 46dead25aa366463835c7cb018a5f7e1aee93e97 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 6 May 2024 14:05:42 +0200 Subject: [PATCH 007/171] Fix: now, evaluation-points are fitted instead of CPs --- splinepy/helpme/create.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 202df67e0..341db4f81 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -3,6 +3,7 @@ import numpy as _np from gustaf.utils import arr as _arr +import splinepy from splinepy import settings as _settings from splinepy.helpme.fit import surface from splinepy.utils import log as _log @@ -359,6 +360,7 @@ def transformation_matrix(traj, v): # set cross section evaluation points along trajectory cps_eval = [] + cs_evals = [] for index, eval_point in enumerate(evals): # evaluate transformation matrix for each trajectory point A = transformation_matrix(trajectory, par_value[index][0]) @@ -374,7 +376,23 @@ def transformation_matrix(traj, v): # append control point to list cps_eval.append(normal_cscp) - fitting_points = _np.array(cps_eval) + cross_sec_placed_cps = _np.array(cps_eval) + number_of_cps_we_take = len(cross_section.control_points) + new_cross_section = splinepy.BSpline( + degrees=cross_section.degrees, + knot_vectors=cross_section.knot_vectors, + control_points=cross_sec_placed_cps[ + index + * number_of_cps_we_take : (index + 1) + * number_of_cps_we_take + ], + ) + + # take care, this has to be the same as the nr of CPs each CS for now + evaluations = new_cross_section.evaluate([[0], [0.33], [0.66], [1]]) + cs_evals.append(evaluations) + + fitting_points = _np.array(cs_evals).reshape(-1, 3) # fit surface interpolated_surface, _ = surface( From 214a35704cfdd4ff8a9c36c652621f1693b32b28 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 7 May 2024 14:30:55 +0200 Subject: [PATCH 008/171] Add: rotation matrix and centered cross section --- examples/show_swept.py | 5 +++++ splinepy/helpme/create.py | 42 +++++++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index d1f675a7c..d238b4385 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -51,10 +51,15 @@ control_points=cps_cs, ) + # user should define the normal vector of the cross section + cs_nv = np.array([0, 0, 1]) + + # create interpolated surface interpolated_surface = splinepy.helpme.create.swept( trajectory=trajectory, cross_section=cross_section, nsections=nsect, + cross_section_normal=cs_nv, ) # testing derivative and evaluation diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 341db4f81..ad91b1857 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -300,7 +300,12 @@ def revolved( return type(spline)(**spline_dict) -def swept(cross_section, trajectory, nsections): +def swept( + cross_section, + trajectory, + nsections, + cross_section_normal=None, +): """Sweeps a cross-section along a trajectory Parameters @@ -311,6 +316,9 @@ def swept(cross_section, trajectory, nsections): Trajectory along which the cross-section is swept nsections : int Number of sections trajectory is divided into + cross_section_normal : np.ndarray + Normal vector of the cross-section + Default is [0, 0, 1] Returns ------- @@ -326,6 +334,9 @@ def swept(cross_section, trajectory, nsections): if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweeps only works for splines") + if cross_section_normal is None: + cross_section_normal = _np.array([0, 0, 1]) + # compute transformation matrix # parameters: trajectory traj and parametric value v def transformation_matrix(traj, v): @@ -338,14 +349,31 @@ def transformation_matrix(traj, v): ) / _np.linalg.norm( _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) ) + if z[0][2] < 0: + z = -z y = _np.cross(z, x) # transformation matrix from global to local coordinates T = [] T = _np.vstack((x, y, z)) + # transformation matrix from local to global coordinates A = _np.linalg.inv(T) - A = _np.negative(A) - return A + + # rotation matrix around y + angle_of_x = _np.arctan2(x[0][2], x[0][0]) + angle_of_cs_normal = _np.arctan2( + cross_section_normal[2], cross_section_normal[0] + ) + rotation_angle = angle_of_cs_normal - angle_of_x + R = _np.array( + [ + [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], + [0, 1, 0], + [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], + ] + ) + + return A, R # compute parameter values for inserting cross sections par_value = _np.zeros((nsections, 1)) @@ -358,15 +386,17 @@ def transformation_matrix(traj, v): # evaluate trajectory at these parameter values evals = trajectory.evaluate(par_value) + # find a center of the cross section + cs_center = cross_section.evaluate([[0.5]]).flatten() + cross_section.control_points = cross_section.control_points - cs_center + # set cross section evaluation points along trajectory cps_eval = [] cs_evals = [] for index, eval_point in enumerate(evals): # evaluate transformation matrix for each trajectory point - A = transformation_matrix(trajectory, par_value[index][0]) + A, R = transformation_matrix(trajectory, par_value[index][0]) for cscp in cross_section.control_points: - # rotation matrix for 90 degrees around y-axis - R = _np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]]) # rotate cross section in trajectory direction normal_cscp = _np.dot(R, cscp) # transform cross section to global coordinates From cf8624203d7b9bf018e46d9e9bc6678071f94fa0 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 7 May 2024 16:36:47 +0200 Subject: [PATCH 009/171] Fix: getting fitting flexible for CS eval points; Note: WIP --- splinepy/helpme/create.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index ad91b1857..73ffa93a7 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -334,6 +334,7 @@ def swept( if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweeps only works for splines") + # setting default value for cross_section_normal if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) @@ -349,6 +350,7 @@ def transformation_matrix(traj, v): ) / _np.linalg.norm( _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) ) + # check if z is pointing in the right direction if z[0][2] < 0: z = -z y = _np.cross(z, x) @@ -372,7 +374,6 @@ def transformation_matrix(traj, v): [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], ] ) - return A, R # compute parameter values for inserting cross sections @@ -386,7 +387,7 @@ def transformation_matrix(traj, v): # evaluate trajectory at these parameter values evals = trajectory.evaluate(par_value) - # find a center of the cross section + # translate cross section to origin cs_center = cross_section.evaluate([[0.5]]).flatten() cross_section.control_points = cross_section.control_points - cs_center @@ -418,22 +419,24 @@ def transformation_matrix(traj, v): ], ) - # take care, this has to be the same as the nr of CPs each CS for now - evaluations = new_cross_section.evaluate([[0], [0.33], [0.66], [1]]) + # evaluate cross section at parameter values + evaluations = new_cross_section.evaluate( + [[0], [0.25], [0.5], [0.75], [1]] + ) cs_evals.append(evaluations) fitting_points = _np.array(cs_evals).reshape(-1, 3) - # fit surface + # fit surface - take care of size and n_control_points --> not sure yet interpolated_surface, _ = surface( fitting_points=fitting_points, size=[ - len(cross_section.control_points), - len(trajectory.control_points), + len(evaluations), + nsections, ], n_control_points=[ - len(cross_section.control_points), - len(trajectory.control_points), + len(evaluations), + nsections, ], degrees=[int(cross_section.degrees), int(trajectory.degrees)], ) From 76e566a0f11a3d02e87899117acb0508940fc969 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 14 May 2024 17:28:20 +0200 Subject: [PATCH 010/171] Add: Transformation matrix now with projection; Note: WIP --- examples/show_swept.py | 8 ++--- splinepy/helpme/create.py | 74 ++++++++++++++++++++++++++++++--------- 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index d238b4385..339b96b4b 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -13,8 +13,8 @@ cps1 = np.array( [ [0.0, 0.0, 0.0], - [3.0, 1.0, 0.0], - [4.0, 0.0, 0.0], + [3.0, 1, 0.0], + [4.0, 3, 0.0], ] ) # init trajectory as bspline @@ -29,10 +29,10 @@ # cross section # define degrees - ds_cs = [2] + ds_cs = [3] # define knot vectors - kvs_cs = [[0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]] + kvs_cs = [[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]] # define control points cps_cs = np.array( diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 73ffa93a7..7f10abfd4 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -330,9 +330,9 @@ def swept( # Check input type if not isinstance(cross_section, _Spline): - raise NotImplementedError("Sweeps only works for splines") + raise NotImplementedError("Sweep only works for splines") if not isinstance(trajectory, _Spline): - raise NotImplementedError("Sweeps only works for splines") + raise NotImplementedError("Sweep only works for splines") # setting default value for cross_section_normal if cross_section_normal is None: @@ -340,29 +340,67 @@ def swept( # compute transformation matrix # parameters: trajectory traj and parametric value v - def transformation_matrix(traj, v): + # def transformation_matrix(traj, v): + # # local directions in global coordinates + # x = traj.derivative([[v]], [1]) / _np.linalg.norm( + # traj.derivative([[v]], [1]) + # ) + # z = _np.cross( + # traj.derivative([[v]], [1]), traj.derivative([[v]], [2]) + # ) / _np.linalg.norm( + # _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2]))) + + # # check if z is pointing in the right direction + # if z[0][2] < 0: + # z = -z + # y = _np.cross(z, x) + # # transformation matrix from global to local coordinates + # T = [] + # 3 + # T = _np.vstack((x, y, z)) + + # # transformation matrix from local to global coordinates + # A = _np.linalg.inv(T) + + # # rotation matrix around y + # angle_of_x = _np.arctan2(x[0][2], x[0][0]) + # angle_of_cs_normal = _np.arctan2( + # cross_section_normal[2], cross_section_normal[0] + # ) + # rotation_angle = angle_of_cs_normal - angle_of_x + # R = _np.array( + # [ + # [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], + # [0, 1, 0], + # [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], + # ] + # ) + # return A, R + + def transformation_matrix_with_projection(traj, v): # local directions in global coordinates - x = traj.derivative([[v]], [1]) / _np.linalg.norm( + x = ( traj.derivative([[v]], [1]) - ) - z = _np.cross( - traj.derivative([[v]], [1]), traj.derivative([[v]], [2]) - ) / _np.linalg.norm( - _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2])) - ) - # check if z is pointing in the right direction - if z[0][2] < 0: - z = -z + / _np.linalg.norm(traj.derivative([[v]], [1])) + ).flatten() + # arbitrary vector B_0 normal to x + vec = [1, 1, 1] + B_0 = _np.cross(x, vec) + # projecting B_0 onto the plane normal to x + b_i = B_0 - _np.dot(B_0, x) * x + z = b_i / _np.linalg.norm(b_i) + y = _np.cross(z, x) # transformation matrix from global to local coordinates T = [] + T = _np.vstack((x, y, z)) # transformation matrix from local to global coordinates A = _np.linalg.inv(T) # rotation matrix around y - angle_of_x = _np.arctan2(x[0][2], x[0][0]) + angle_of_x = _np.arctan2(x[2], x[0]) angle_of_cs_normal = _np.arctan2( cross_section_normal[2], cross_section_normal[0] ) @@ -396,7 +434,9 @@ def transformation_matrix(traj, v): cs_evals = [] for index, eval_point in enumerate(evals): # evaluate transformation matrix for each trajectory point - A, R = transformation_matrix(trajectory, par_value[index][0]) + A, R = transformation_matrix_with_projection( + trajectory, par_value[index][0] + ) for cscp in cross_section.control_points: # rotate cross section in trajectory direction normal_cscp = _np.dot(R, cscp) @@ -435,8 +475,8 @@ def transformation_matrix(traj, v): nsections, ], n_control_points=[ - len(evaluations), - nsections, + len(cross_section.control_points), + len(trajectory.control_points), ], degrees=[int(cross_section.degrees), int(trajectory.degrees)], ) From 60eeceb049304becd2224fd13f7f27401def5e31 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 15 May 2024 16:17:49 +0200 Subject: [PATCH 011/171] Fix: calculation of transformation matrix - still working on projection; Note: WIP --- examples/show_swept.py | 8 ++--- splinepy/helpme/create.py | 72 ++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 339b96b4b..d238b4385 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -13,8 +13,8 @@ cps1 = np.array( [ [0.0, 0.0, 0.0], - [3.0, 1, 0.0], - [4.0, 3, 0.0], + [3.0, 1.0, 0.0], + [4.0, 0.0, 0.0], ] ) # init trajectory as bspline @@ -29,10 +29,10 @@ # cross section # define degrees - ds_cs = [3] + ds_cs = [2] # define knot vectors - kvs_cs = [[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]] + kvs_cs = [[0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]] # define control points cps_cs = np.array( diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 7f10abfd4..a756b3084 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -356,7 +356,6 @@ def swept( # y = _np.cross(z, x) # # transformation matrix from global to local coordinates # T = [] - # 3 # T = _np.vstack((x, y, z)) # # transformation matrix from local to global coordinates @@ -377,41 +376,54 @@ def swept( # ) # return A, R - def transformation_matrix_with_projection(traj, v): - # local directions in global coordinates - x = ( - traj.derivative([[v]], [1]) - / _np.linalg.norm(traj.derivative([[v]], [1])) - ).flatten() + def transformation_matrix_with_projection(traj, index, par_value): + # for i in range(len(traj.knot_vectors[0])): + x = traj.derivative([[0]], [1]) / _np.linalg.norm( + traj.derivative([[0]], [1]) + ) # arbitrary vector B_0 normal to x - vec = [1, 1, 1] - B_0 = _np.cross(x, vec) - # projecting B_0 onto the plane normal to x - b_i = B_0 - _np.dot(B_0, x) * x - z = b_i / _np.linalg.norm(b_i) + vec = [1.5, 0.3, 1] + B = [] + B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) + z = B[0] + + for i in range(1, index + 1): + # local directions in global coordinates + x = ( + traj.derivative([[par_value[i][0]]], [1]) + / _np.linalg.norm(traj.derivative([[par_value[i][0]]], [1])) + ).flatten() + + # projecting B_i onto the plane normal to x + B.append(B[i - 1] - _np.dot(B[i - 1], x) * x) + B[i] = B[i] / _np.linalg.norm(B[i]) + z = B[i] y = _np.cross(z, x) - # transformation matrix from global to local coordinates - T = [] + # transformation matrix from global to local coordinates T = _np.vstack((x, y, z)) # transformation matrix from local to global coordinates A = _np.linalg.inv(T) # rotation matrix around y - angle_of_x = _np.arctan2(x[2], x[0]) - angle_of_cs_normal = _np.arctan2( - cross_section_normal[2], cross_section_normal[0] - ) - rotation_angle = angle_of_cs_normal - angle_of_x - R = _np.array( - [ - [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], - [0, 1, 0], - [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], - ] - ) + # angle_of_x = _np.arctan2(x[2], x[0]) + # angle_of_cs_normal = _np.arctan2( + # cross_section_normal[2], cross_section_normal[0] + # ) + # rotation_angle = angle_of_cs_normal - angle_of_x + # R = [] + # R.append(_np.array( + # [ + # [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], + # [0, 1, 0], + # [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], + # ] + # )) + + # rotation matrix for rotation 90° around z-axis + R = _np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]]) return A, R # compute parameter values for inserting cross sections @@ -429,16 +441,22 @@ def transformation_matrix_with_projection(traj, v): cs_center = cross_section.evaluate([[0.5]]).flatten() cross_section.control_points = cross_section.control_points - cs_center + # # evaluate transformation matrix for each trajectory point + # A, R, B = transformation_matrix_with_projection( + # trajectory + # ) + # set cross section evaluation points along trajectory cps_eval = [] cs_evals = [] for index, eval_point in enumerate(evals): # evaluate transformation matrix for each trajectory point A, R = transformation_matrix_with_projection( - trajectory, par_value[index][0] + trajectory, index, par_value ) for cscp in cross_section.control_points: # rotate cross section in trajectory direction + # cscp = cscp.reshape(-1, 1) normal_cscp = _np.dot(R, cscp) # transform cross section to global coordinates normal_cscp = _np.dot(A, normal_cscp) From c6bee4ff02c8b61158e4ba27b7744032035663a7 Mon Sep 17 00:00:00 2001 From: Jaewook Lee Date: Tue, 21 May 2024 15:37:30 +0200 Subject: [PATCH 012/171] minor clean up during discussion --- splinepy/helpme/create.py | 41 ++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index a756b3084..8f4930967 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -303,7 +303,7 @@ def revolved( def swept( cross_section, trajectory, - nsections, + nsections=None, cross_section_normal=None, ): """Sweeps a cross-section along a trajectory @@ -338,6 +338,10 @@ def swept( if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) + # make copies so we can work on it inplace + trajectory = trajectory.copy() + cross_section = cross_section.copy() + # compute transformation matrix # parameters: trajectory traj and parametric value v # def transformation_matrix(traj, v): @@ -378,8 +382,8 @@ def swept( def transformation_matrix_with_projection(traj, index, par_value): # for i in range(len(traj.knot_vectors[0])): - x = traj.derivative([[0]], [1]) / _np.linalg.norm( - traj.derivative([[0]], [1]) + x = traj.derivative([par_value[0]], [1]) / _np.linalg.norm( + traj.derivative([par_value[0]], [1]) ) # arbitrary vector B_0 normal to x vec = [1.5, 0.3, 1] @@ -390,8 +394,8 @@ def transformation_matrix_with_projection(traj, index, par_value): for i in range(1, index + 1): # local directions in global coordinates x = ( - traj.derivative([[par_value[i][0]]], [1]) - / _np.linalg.norm(traj.derivative([[par_value[i][0]]], [1])) + traj.derivative([par_value[i]], [1]) + / _np.linalg.norm(traj.derivative([par_value[i]], [1])) ).flatten() # projecting B_i onto the plane normal to x @@ -427,19 +431,22 @@ def transformation_matrix_with_projection(traj, index, par_value): return A, R # compute parameter values for inserting cross sections - par_value = _np.zeros((nsections, 1)) - for i in range(nsections): - par_value[i][0] = ( - trajectory.knot_vectors[0][i + 1] - + trajectory.knot_vectors[0][i + 2] - ) / 2 + if nsections is None: + par_value = trajectory.greville_abscssae() + else: + bounds = trajectory.parametric_bounds.ravel() + par_value = _np.linspace(*bounds, nsections) + par_value = par_value.reshape(-1, 1) # evaluate trajectory at these parameter values evals = trajectory.evaluate(par_value) # translate cross section to origin - cs_center = cross_section.evaluate([[0.5]]).flatten() - cross_section.control_points = cross_section.control_points - cs_center + cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) + cs_center = cross_section.evaluate( + cross_para_center.reshape(-1, 1) + ).ravel() + cross_section.control_points -= cs_center # # evaluate transformation matrix for each trajectory point # A, R, B = transformation_matrix_with_projection( @@ -457,9 +464,9 @@ def transformation_matrix_with_projection(traj, index, par_value): for cscp in cross_section.control_points: # rotate cross section in trajectory direction # cscp = cscp.reshape(-1, 1) - normal_cscp = _np.dot(R, cscp) + normal_cscp = _np.matmul(R, cscp) # transform cross section to global coordinates - normal_cscp = _np.dot(A, normal_cscp) + normal_cscp = _np.matmul(A, normal_cscp) # translate cross section to trajectory point normal_cscp = normal_cscp + eval_point # append control point to list @@ -478,9 +485,7 @@ def transformation_matrix_with_projection(traj, index, par_value): ) # evaluate cross section at parameter values - evaluations = new_cross_section.evaluate( - [[0], [0.25], [0.5], [0.75], [1]] - ) + evaluations = new_cross_section.sample(7) cs_evals.append(evaluations) fitting_points = _np.array(cs_evals).reshape(-1, 3) From d74eafa26f976ab230a3bf0dc11c9c299ea5179d Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 22 May 2024 20:28:49 +0200 Subject: [PATCH 013/171] Fix: create-file transformation matrices now calculated once; rotation works in this case --- splinepy/helpme/create.py | 68 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 8f4930967..26c25d3f6 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -344,7 +344,7 @@ def swept( # compute transformation matrix # parameters: trajectory traj and parametric value v - # def transformation_matrix(traj, v): + # def transformation_matrix_without_projection(traj, v): # # local directions in global coordinates # x = traj.derivative([[v]], [1]) / _np.linalg.norm( # traj.derivative([[v]], [1]) @@ -380,36 +380,42 @@ def swept( # ) # return A, R - def transformation_matrix_with_projection(traj, index, par_value): - # for i in range(len(traj.knot_vectors[0])): + def transformation_matrices(traj, par_value): + # tangent vector x on trajectory at parameter value 0 x = traj.derivative([par_value[0]], [1]) / _np.linalg.norm( traj.derivative([par_value[0]], [1]) ) - # arbitrary vector B_0 normal to x + + # evaluating arbitrary vector B_0 normal to x vec = [1.5, 0.3, 1] B = [] B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) - z = B[0] - for i in range(1, index + 1): - # local directions in global coordinates + # initializing transformation matrices + T = [] + A = [] + + # evaluating transformation matrices for each trajectory point + for i in range(len(par_value)): + # tangent vector x on trajectory at parameter value i x = ( traj.derivative([par_value[i]], [1]) / _np.linalg.norm(traj.derivative([par_value[i]], [1])) ).flatten() - # projecting B_i onto the plane normal to x - B.append(B[i - 1] - _np.dot(B[i - 1], x) * x) - B[i] = B[i] / _np.linalg.norm(B[i]) - z = B[i] + # projecting B_(i-1) onto the plane normal to x + B.append(B[i] - _np.dot(B[i], x) * x) + B[i + 1] = B[i + 1] / _np.linalg.norm(B[i + 1]) - y = _np.cross(z, x) + # defining y and z axis-vectors + z = B[i + 1] + y = _np.cross(z, x) - # transformation matrix from global to local coordinates - T = _np.vstack((x, y, z)) + # array of transformation matrices from global to local coordinates + T.append(_np.vstack((x, y, z))) - # transformation matrix from local to global coordinates - A = _np.linalg.inv(T) + # array of transformation matrices from local to global coordinates + A.append(_np.linalg.inv(T[i])) # rotation matrix around y # angle_of_x = _np.arctan2(x[2], x[0]) @@ -426,8 +432,8 @@ def transformation_matrix_with_projection(traj, index, par_value): # ] # )) - # rotation matrix for rotation 90° around z-axis - R = _np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]]) + # rotation matrix for rotation 90° around z-axis and 90° around y-axis + R = _np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]]) return A, R # compute parameter values for inserting cross sections @@ -441,40 +447,35 @@ def transformation_matrix_with_projection(traj, index, par_value): # evaluate trajectory at these parameter values evals = trajectory.evaluate(par_value) - # translate cross section to origin + # evaluate center of cross section and translate to origin cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) cs_center = cross_section.evaluate( cross_para_center.reshape(-1, 1) ).ravel() cross_section.control_points -= cs_center - # # evaluate transformation matrix for each trajectory point - # A, R, B = transformation_matrix_with_projection( - # trajectory - # ) + # evaluate transformation matrices for every trajectory point + A, R = transformation_matrices(trajectory, par_value) # set cross section evaluation points along trajectory cps_eval = [] cs_evals = [] for index, eval_point in enumerate(evals): - # evaluate transformation matrix for each trajectory point - A, R = transformation_matrix_with_projection( - trajectory, index, par_value - ) + # place every control point of cross section separately for cscp in cross_section.control_points: # rotate cross section in trajectory direction - # cscp = cscp.reshape(-1, 1) normal_cscp = _np.matmul(R, cscp) # transform cross section to global coordinates - normal_cscp = _np.matmul(A, normal_cscp) + normal_cscp = _np.matmul(A[index], normal_cscp) # translate cross section to trajectory point normal_cscp = normal_cscp + eval_point # append control point to list cps_eval.append(normal_cscp) + # create spline of new cross section cross_sec_placed_cps = _np.array(cps_eval) number_of_cps_we_take = len(cross_section.control_points) - new_cross_section = splinepy.BSpline( + temp_cross_section = splinepy.BSpline( degrees=cross_section.degrees, knot_vectors=cross_section.knot_vectors, control_points=cross_sec_placed_cps[ @@ -485,16 +486,17 @@ def transformation_matrix_with_projection(traj, index, par_value): ) # evaluate cross section at parameter values - evaluations = new_cross_section.sample(7) - cs_evals.append(evaluations) + temp_evaluations = temp_cross_section.sample(7) + cs_evals.append(temp_evaluations) + # defining points for fitting routine fitting_points = _np.array(cs_evals).reshape(-1, 3) # fit surface - take care of size and n_control_points --> not sure yet interpolated_surface, _ = surface( fitting_points=fitting_points, size=[ - len(evaluations), + len(temp_evaluations), nsections, ], n_control_points=[ From 330d62e1a0fab28decade1b5aa1d609d9563e5d9 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 22 May 2024 21:56:24 +0200 Subject: [PATCH 014/171] Fix: changed arbitrary vector vec and rotation matrix in create-file; changed trajectory in show_swept-file --- examples/show_swept.py | 9 ++++----- splinepy/helpme/create.py | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index d238b4385..f128ed44e 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -13,8 +13,8 @@ cps1 = np.array( [ [0.0, 0.0, 0.0], - [3.0, 1.0, 0.0], - [4.0, 0.0, 0.0], + [3.0, 1.0, 0.5], + [4.0, 0.0, 2.0], ] ) # init trajectory as bspline @@ -32,15 +32,14 @@ ds_cs = [2] # define knot vectors - kvs_cs = [[0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]] + kvs_cs = [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]] # define control points cps_cs = np.array( [ [0.0, 0.0, 0.0], - [1.0, 1.0, 0.0], + [1.0, 2.0, 0.0], [2.0, 0.0, 0.0], - [3.0, 1.0, 0.0], ] ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 26c25d3f6..ec120b6eb 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -387,7 +387,7 @@ def transformation_matrices(traj, par_value): ) # evaluating arbitrary vector B_0 normal to x - vec = [1.5, 0.3, 1] + vec = [0, 1, 0] B = [] B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) @@ -432,8 +432,8 @@ def transformation_matrices(traj, par_value): # ] # )) - # rotation matrix for rotation 90° around z-axis and 90° around y-axis - R = _np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]]) + # rotation matrix for rotation 90° around y-axis + R = _np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) return A, R # compute parameter values for inserting cross sections From d4f6946d1db3db7e55331a081f0873a1a57a8779 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 23 May 2024 12:09:55 +0200 Subject: [PATCH 015/171] Fix: random vector vec now dependent of tangent --- examples/show_swept.py | 2 +- splinepy/helpme/create.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index f128ed44e..cd90ffe52 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -13,7 +13,7 @@ cps1 = np.array( [ [0.0, 0.0, 0.0], - [3.0, 1.0, 0.5], + [2.0, 1.0, 1.0], [4.0, 0.0, 2.0], ] ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index ec120b6eb..777f33f44 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -339,7 +339,7 @@ def swept( cross_section_normal = _np.array([0, 0, 1]) # make copies so we can work on it inplace - trajectory = trajectory.copy() + trajectory = trajectory.create.embedded(3) cross_section = cross_section.copy() # compute transformation matrix @@ -382,12 +382,13 @@ def swept( def transformation_matrices(traj, par_value): # tangent vector x on trajectory at parameter value 0 - x = traj.derivative([par_value[0]], [1]) / _np.linalg.norm( + x = ( traj.derivative([par_value[0]], [1]) - ) + / _np.linalg.norm(traj.derivative([par_value[0]], [1])) + ).ravel() - # evaluating arbitrary vector B_0 normal to x - vec = [0, 1, 0] + # evaluating a vector B_0 normal to x + vec = [-x[1], x[0], -x[2]] B = [] B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) From 61e15b63235363719a1f2e8d31054b68080619d8 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Sun, 9 Jun 2024 19:59:41 +0200 Subject: [PATCH 016/171] Add: new function in fit.py-file; Fix: adaptions in create.py-file acc to new func --- examples/show_swept.py | 19 +++++--- splinepy/helpme/create.py | 44 ++++++++++-------- splinepy/helpme/fit.py | 96 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 27 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index cd90ffe52..d63186afd 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -8,31 +8,34 @@ # define degrees ds1 = [2] # define knot vectors - kvs1 = [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]] + kvs1 = [[0.0, 0.0, 0.0, 0.25, 0.75, 1.0, 1.0, 1.0]] # define control points cps1 = np.array( [ [0.0, 0.0, 0.0], - [2.0, 1.0, 1.0], - [4.0, 0.0, 2.0], + [5.0, 2.0, 4.0], + [10.0, 5.0, 8.0], + [15.0, 2.0, 4.0], + [20.0, 5.0, 0.0], ] ) + # init trajectory as bspline trajectory = splinepy.BSpline( degrees=ds1, knot_vectors=kvs1, - control_points=cps1, - ) + control_points=cps1,) + # define sections along trajectory nsect = len(kvs1[0]) - ds1[0] - 1 # cross section # define degrees - ds_cs = [2] + ds_cs = [3] # define knot vectors - kvs_cs = [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]] + kvs_cs = [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]] # define control points cps_cs = np.array( @@ -40,6 +43,8 @@ [0.0, 0.0, 0.0], [1.0, 2.0, 0.0], [2.0, 0.0, 0.0], + [3.0, -2.0, 0.0], + [4.0, 0.0, 0.0], ] ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 777f33f44..287f66fd2 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -5,7 +5,7 @@ import splinepy from splinepy import settings as _settings -from splinepy.helpme.fit import surface +from splinepy.helpme.fit import surface_from_cross_sections from splinepy.utils import log as _log from splinepy.utils.data import make_matrix as _make_matrix @@ -459,9 +459,10 @@ def transformation_matrices(traj, par_value): A, R = transformation_matrices(trajectory, par_value) # set cross section evaluation points along trajectory - cps_eval = [] - cs_evals = [] + cps = [] + cross_section_splines = [] for index, eval_point in enumerate(evals): + temp_csp = [] # place every control point of cross section separately for cscp in cross_section.control_points: # rotate cross section in trajectory direction @@ -471,33 +472,36 @@ def transformation_matrices(traj, par_value): # translate cross section to trajectory point normal_cscp = normal_cscp + eval_point # append control point to list - cps_eval.append(normal_cscp) + temp_csp.append(normal_cscp) - # create spline of new cross section - cross_sec_placed_cps = _np.array(cps_eval) - number_of_cps_we_take = len(cross_section.control_points) + # create temp spline of new cross section + cross_sec_placed_cps = _np.array(temp_csp) temp_cross_section = splinepy.BSpline( degrees=cross_section.degrees, knot_vectors=cross_section.knot_vectors, - control_points=cross_sec_placed_cps[ - index - * number_of_cps_we_take : (index + 1) - * number_of_cps_we_take - ], + control_points=cross_sec_placed_cps ) + + # append cross section spline to list + cross_section_splines.append(temp_cross_section) + cps.append(cross_sec_placed_cps) - # evaluate cross section at parameter values - temp_evaluations = temp_cross_section.sample(7) - cs_evals.append(temp_evaluations) + cps = _np.array(cps) + degrees=[int(cross_section.degrees), int(trajectory.degrees)] + knot_vectors=[cross_section.knot_vectors[0][:], trajectory.knot_vectors[0][:]] - # defining points for fitting routine - fitting_points = _np.array(cs_evals).reshape(-1, 3) + fitting_surface = splinepy.BSpline( + degrees, + knot_vectors, + control_points=_np.array(cps).reshape(-1,3),) # fit surface - take care of size and n_control_points --> not sure yet - interpolated_surface, _ = surface( - fitting_points=fitting_points, + interpolated_surface, _ = surface_from_cross_sections( + fitting_spline=fitting_surface, + cross_section_splines=cross_section_splines, + cps=cps, size=[ - len(temp_evaluations), + len(cross_section.control_points), nsections, ], n_control_points=[ diff --git a/splinepy/helpme/fit.py b/splinepy/helpme/fit.py index 3450dd96a..01206153a 100644 --- a/splinepy/helpme/fit.py +++ b/splinepy/helpme/fit.py @@ -5,6 +5,7 @@ from splinepy.utils import log as _log from splinepy.utils.data import has_scipy as _has_scipy from splinepy.utils.data import make_matrix as _make_matrix +import splinepy if _has_scipy: from scipy.sparse.linalg import spsolve as _spsolve @@ -589,3 +590,98 @@ def surface( "residual": residual, } return fitted_spline, residual + +def surface_from_cross_sections( + cross_section_splines, + cps, + size, + degrees=None, + n_control_points=None, + knot_vectors=None, + fitting_spline=None, + associated_queries=None, + centripetal=True, + interpolate_endpoints=True, + verbose_output=False, +): + """ + ... + + Parameters + ---------- + ... + Returns + ------- + ... + """ + + # dim = fitting_points.shape[1] + dim = 3 + + # get evaluation locations + u_k = [None, None] + if associated_queries is not None and ( + knot_vectors is not None or fitting_spline is not None + ): + # check dimensions of queries + if ( + associated_queries[0].shape[1] != 1 + or associated_queries[1].shape[1] != 1 + ): + raise ValueError( + "Associated queries in each direction must have dimension 1!" + ) + u_k[0] = _np.asanyarray(associated_queries[0]) + u_k[1] = _np.asanyarray(associated_queries[1]) + + if fitting_spline.para_dim != 2: + raise ValueError( + "If fitting spline is given as one spline, " + "parametric dimension must be 2!" + ) + + # extract spline in each direction (knot_vectors, weights etc. + # of original spline), works for all type of splines! + fitting_splines = fitting_spline.extract.boundaries([2, 0]) + fitted_spline = fitting_spline.copy() + + n_control_points = fitted_spline.control_mesh_resolutions + + # index helper + mi_interim_cps = MultiIndex((n_control_points[0], size[1])) + + # loop first dim + # curve fit for every j in n_points_v + interim_control_points = _np.empty((n_control_points[0] * size[1], dim)) + # residual_u = _np.empty(size[1]) + # fitted_spline_u = fitting_splines[0] + for v in range(len(cross_section_splines)): + interim_control_points[mi_interim_cps[:, v]] = ( + cross_section_splines[v].control_points + ) + + # loop trajectory dim + # curve fit for every k in n_control_points_u + residual_v = _np.empty(n_control_points[0]) + fitted_spline_v = fitting_splines[1] + for u in range(n_control_points[0]): + fitted_spline_v, residual_v[u] = curve( + fitting_points=cps[:,u], + fitting_spline=fitted_spline_v, + #associated_queries=u_k[1], + centripetal=centripetal, + interpolate_endpoints=interpolate_endpoints, + ) + + interim_control_points[mi_interim_cps[u, : n_control_points[1]]] = ( + fitted_spline_v.control_points + ) + + # copy + fitted_spline.control_points = interim_control_points[ + mi_interim_cps[: n_control_points[0], : n_control_points[1]] + ] + + residual = _np.linalg.norm(residual_v) + + return fitted_spline, residual From b2d3960d9355b4a7755b0176ac7429c387d559f4 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Sun, 9 Jun 2024 20:04:04 +0200 Subject: [PATCH 017/171] Fix: pre-commit changes --- examples/show_swept.py | 4 ++-- splinepy/helpme/create.py | 20 ++++++++++++-------- splinepy/helpme/fit.py | 20 ++++++++++---------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index d63186afd..4c8f131e4 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -24,8 +24,8 @@ trajectory = splinepy.BSpline( degrees=ds1, knot_vectors=kvs1, - control_points=cps1,) - + control_points=cps1, + ) # define sections along trajectory nsect = len(kvs1[0]) - ds1[0] - 1 diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 287f66fd2..81c309155 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -479,21 +479,25 @@ def transformation_matrices(traj, par_value): temp_cross_section = splinepy.BSpline( degrees=cross_section.degrees, knot_vectors=cross_section.knot_vectors, - control_points=cross_sec_placed_cps + control_points=cross_sec_placed_cps, ) - + # append cross section spline to list cross_section_splines.append(temp_cross_section) cps.append(cross_sec_placed_cps) cps = _np.array(cps) - degrees=[int(cross_section.degrees), int(trajectory.degrees)] - knot_vectors=[cross_section.knot_vectors[0][:], trajectory.knot_vectors[0][:]] + degrees = [int(cross_section.degrees), int(trajectory.degrees)] + knot_vectors = [ + cross_section.knot_vectors[0][:], + trajectory.knot_vectors[0][:], + ] fitting_surface = splinepy.BSpline( - degrees, - knot_vectors, - control_points=_np.array(cps).reshape(-1,3),) + degrees, + knot_vectors, + control_points=_np.array(cps).reshape(-1, 3), + ) # fit surface - take care of size and n_control_points --> not sure yet interpolated_surface, _ = surface_from_cross_sections( @@ -508,7 +512,7 @@ def transformation_matrices(traj, par_value): len(cross_section.control_points), len(trajectory.control_points), ], - degrees=[int(cross_section.degrees), int(trajectory.degrees)], + # degrees=[int(cross_section.degrees), int(trajectory.degrees)], ) return interpolated_surface diff --git a/splinepy/helpme/fit.py b/splinepy/helpme/fit.py index 01206153a..2c6f59cf0 100644 --- a/splinepy/helpme/fit.py +++ b/splinepy/helpme/fit.py @@ -5,7 +5,6 @@ from splinepy.utils import log as _log from splinepy.utils.data import has_scipy as _has_scipy from splinepy.utils.data import make_matrix as _make_matrix -import splinepy if _has_scipy: from scipy.sparse.linalg import spsolve as _spsolve @@ -591,18 +590,19 @@ def surface( } return fitted_spline, residual + def surface_from_cross_sections( cross_section_splines, cps, size, - degrees=None, + # degrees=None, n_control_points=None, knot_vectors=None, fitting_spline=None, associated_queries=None, centripetal=True, interpolate_endpoints=True, - verbose_output=False, + # verbose_output=False, ): """ ... @@ -614,7 +614,7 @@ def surface_from_cross_sections( ------- ... """ - + # dim = fitting_points.shape[1] dim = 3 @@ -639,7 +639,7 @@ def surface_from_cross_sections( "If fitting spline is given as one spline, " "parametric dimension must be 2!" ) - + # extract spline in each direction (knot_vectors, weights etc. # of original spline), works for all type of splines! fitting_splines = fitting_spline.extract.boundaries([2, 0]) @@ -656,9 +656,9 @@ def surface_from_cross_sections( # residual_u = _np.empty(size[1]) # fitted_spline_u = fitting_splines[0] for v in range(len(cross_section_splines)): - interim_control_points[mi_interim_cps[:, v]] = ( - cross_section_splines[v].control_points - ) + interim_control_points[mi_interim_cps[:, v]] = cross_section_splines[ + v + ].control_points # loop trajectory dim # curve fit for every k in n_control_points_u @@ -666,9 +666,9 @@ def surface_from_cross_sections( fitted_spline_v = fitting_splines[1] for u in range(n_control_points[0]): fitted_spline_v, residual_v[u] = curve( - fitting_points=cps[:,u], + fitting_points=cps[:, u], fitting_spline=fitted_spline_v, - #associated_queries=u_k[1], + # associated_queries=u_k[1], centripetal=centripetal, interpolate_endpoints=interpolate_endpoints, ) From 14c4d417472b3b843f22076843539cb2f2df84e1 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 12 Jun 2024 11:21:52 +0200 Subject: [PATCH 018/171] Add: changed show-settings to better possibility of comparison --- examples/show_swept.py | 7 +++++-- splinepy/helpme/create.py | 2 +- splinepy/helpme/fit.py | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 4c8f131e4..fde1869e2 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -59,7 +59,7 @@ cs_nv = np.array([0, 0, 1]) # create interpolated surface - interpolated_surface = splinepy.helpme.create.swept( + interpolated_surface, fitting_surface = splinepy.helpme.create.swept( trajectory=trajectory, cross_section=cross_section, nsections=nsect, @@ -84,6 +84,7 @@ def der(data, on): trajectory.show_options["control_mesh"] = False cross_section.show_options["control_mesh"] = False interpolated_surface.show_options["control_mesh"] = False + fitting_surface.show_options["control_mesh"] = False gus.show( ["Trajectory", trajectory], @@ -92,6 +93,8 @@ def der(data, on): ) gus.show( - ["Surface", interpolated_surface], + ["Trajectory", trajectory], + ["Fitting Surface", fitting_surface], + ["Fitted Surface", interpolated_surface], resolution=50, ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 81c309155..2f996c0ec 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -515,7 +515,7 @@ def transformation_matrices(traj, par_value): # degrees=[int(cross_section.degrees), int(trajectory.degrees)], ) - return interpolated_surface + return interpolated_surface, fitting_surface def from_bounds(parametric_bounds, physical_bounds): diff --git a/splinepy/helpme/fit.py b/splinepy/helpme/fit.py index 2c6f59cf0..8c6002e97 100644 --- a/splinepy/helpme/fit.py +++ b/splinepy/helpme/fit.py @@ -649,7 +649,6 @@ def surface_from_cross_sections( # index helper mi_interim_cps = MultiIndex((n_control_points[0], size[1])) - # loop first dim # curve fit for every j in n_points_v interim_control_points = _np.empty((n_control_points[0] * size[1], dim)) From 9fa62b115bc80558dad7782daac0663cb1b8c317 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 12 Jun 2024 17:07:23 +0200 Subject: [PATCH 019/171] Big update; Add: nurbs, traj refinement; Del: fitting routine --- examples/show_swept.py | 120 ++++++++++---------------- splinepy/helpme/create.py | 171 +++++++++++++++----------------------- splinepy/helpme/fit.py | 95 --------------------- 3 files changed, 113 insertions(+), 273 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index fde1869e2..9a0d9cc55 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -4,97 +4,69 @@ import splinepy if __name__ == "__main__": - # trajectory - # define degrees - ds1 = [2] - # define knot vectors - kvs1 = [[0.0, 0.0, 0.0, 0.25, 0.75, 1.0, 1.0, 1.0]] - # define control points - cps1 = np.array( - [ - [0.0, 0.0, 0.0], - [5.0, 2.0, 4.0], - [10.0, 5.0, 8.0], - [15.0, 2.0, 4.0], - [20.0, 5.0, 0.0], - ] - ) - - # init trajectory as bspline - trajectory = splinepy.BSpline( - degrees=ds1, - knot_vectors=kvs1, - control_points=cps1, - ) - # define sections along trajectory - nsect = len(kvs1[0]) - ds1[0] - 1 + ### TRAJECTORY ### + dict_trajectory = { + "degrees": [2], + "knot_vectors": [[0.0, 0.0, 0.0, 0.333, 0.666, 1.0, 1.0, 1.0]], + "control_points": np.array( + [ + [0.0, 0.0, 0.0], + [5.0, 5.0, 0.0], + [10.0, 7.5, 0.0], + [10.0, 5.0, 0.0], + [10.0, 0.0, 0.0], + ] + ), + } - # cross section - # define degrees - ds_cs = [3] + # init trajectory as bspline + trajectory = splinepy.BSpline(**dict_trajectory) - # define knot vectors - kvs_cs = [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]] + # insert knots and control points + # trajectory.uniform_refine([0], 1) - # define control points - cps_cs = np.array( - [ - [0.0, 0.0, 0.0], - [1.0, 2.0, 0.0], - [2.0, 0.0, 0.0], - [3.0, -2.0, 0.0], - [4.0, 0.0, 0.0], - ] - ) + ### CROSS SECTION ### + dict_cross_section = { + "degrees": [3], + "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], + "control_points": np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 2.0, 0.0], + [2.0, 0.0, 0.0], + [3.0, -2.0, 0.0], + [4.0, 0.0, 0.0], + ] + ), + } # init cross section as bspline - cross_section = splinepy.BSpline( - degrees=ds_cs, - knot_vectors=kvs_cs, - control_points=cps_cs, - ) + cross_section = splinepy.BSpline(**dict_cross_section) + + # alternatively, use helpme to create a cross section + # cross_section = splinepy.helpme.create.surface_circle(.5).nurbs - # user should define the normal vector of the cross section + # user can define the normal vector of the cross section, in case + # the cross section is not planar in the x-y plane (default) cs_nv = np.array([0, 0, 1]) - # create interpolated surface - interpolated_surface, fitting_surface = splinepy.helpme.create.swept( + ### SWEEP ### + swept_surface = splinepy.helpme.create.swept( trajectory=trajectory, cross_section=cross_section, - nsections=nsect, + nsections=20, cross_section_normal=cs_nv, ) - # testing derivative and evaluation - # derivative = trajectory.derivative([[0.5], [1]], [1]) - # evals = trajectory.evaluate([[0.5]]) - - # show - def der(data, on): - return data.derivative(on, [1]) - - trajectory.spline_data["der"] = splinepy.SplineDataAdaptor( - trajectory, function=der - ) - trajectory.show_options["arrow_data"] = "der" - trajectory.show_options["arrow_data_on"] = np.linspace(0, 1, 20).reshape( - -1, 1 - ) + ### VISUALIZATION ### trajectory.show_options["control_mesh"] = False cross_section.show_options["control_mesh"] = False - interpolated_surface.show_options["control_mesh"] = False - fitting_surface.show_options["control_mesh"] = False - - gus.show( - ["Trajectory", trajectory], - ["Cross section", cross_section], - resolution=50, - ) + swept_surface.show_options["control_mesh"] = False gus.show( ["Trajectory", trajectory], - ["Fitting Surface", fitting_surface], - ["Fitted Surface", interpolated_surface], - resolution=50, + ["Cross Section", cross_section], + ["Swept Surface", swept_surface], + resolution=51, ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 2f996c0ec..0c28a531e 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -5,7 +5,6 @@ import splinepy from splinepy import settings as _settings -from splinepy.helpme.fit import surface_from_cross_sections from splinepy.utils import log as _log from splinepy.utils.data import make_matrix as _make_matrix @@ -328,7 +327,7 @@ def swept( from splinepy.spline import Spline as _Spline - # Check input type + # check input type if not isinstance(cross_section, _Spline): raise NotImplementedError("Sweep only works for splines") if not isinstance(trajectory, _Spline): @@ -340,45 +339,7 @@ def swept( # make copies so we can work on it inplace trajectory = trajectory.create.embedded(3) - cross_section = cross_section.copy() - - # compute transformation matrix - # parameters: trajectory traj and parametric value v - # def transformation_matrix_without_projection(traj, v): - # # local directions in global coordinates - # x = traj.derivative([[v]], [1]) / _np.linalg.norm( - # traj.derivative([[v]], [1]) - # ) - # z = _np.cross( - # traj.derivative([[v]], [1]), traj.derivative([[v]], [2]) - # ) / _np.linalg.norm( - # _np.cross(traj.derivative([[v]], [1]), traj.derivative([[v]], [2]))) - - # # check if z is pointing in the right direction - # if z[0][2] < 0: - # z = -z - # y = _np.cross(z, x) - # # transformation matrix from global to local coordinates - # T = [] - # T = _np.vstack((x, y, z)) - - # # transformation matrix from local to global coordinates - # A = _np.linalg.inv(T) - - # # rotation matrix around y - # angle_of_x = _np.arctan2(x[0][2], x[0][0]) - # angle_of_cs_normal = _np.arctan2( - # cross_section_normal[2], cross_section_normal[0] - # ) - # rotation_angle = angle_of_cs_normal - angle_of_x - # R = _np.array( - # [ - # [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], - # [0, 1, 0], - # [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], - # ] - # ) - # return A, R + cross_section = cross_section.create.embedded(3) def transformation_matrices(traj, par_value): # tangent vector x on trajectory at parameter value 0 @@ -392,7 +353,7 @@ def transformation_matrices(traj, par_value): B = [] B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) - # initializing transformation matrices + # initialize transformation matrices T = [] A = [] @@ -419,30 +380,39 @@ def transformation_matrices(traj, par_value): A.append(_np.linalg.inv(T[i])) # rotation matrix around y - # angle_of_x = _np.arctan2(x[2], x[0]) - # angle_of_cs_normal = _np.arctan2( - # cross_section_normal[2], cross_section_normal[0] - # ) - # rotation_angle = angle_of_cs_normal - angle_of_x - # R = [] - # R.append(_np.array( - # [ - # [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], - # [0, 1, 0], - # [-_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], - # ] - # )) - - # rotation matrix for rotation 90° around y-axis - R = _np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) + angle_of_x = _np.arctan2(x[2], x[0]) + angle_of_cs_normal = _np.arctan2( + cross_section_normal[2], cross_section_normal[0] + ) + rotation_angle = angle_of_cs_normal - angle_of_x + R = _np.array( + [ + [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], + [0, 1, 0], + [_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], + ] + ) + R = _np.where(_np.abs(R) < 1e-10, 0, R) + return A, R # compute parameter values for inserting cross sections if nsections is None: - par_value = trajectory.greville_abscssae() + par_value = trajectory.greville_abscissae() + v_dir_kv = trajectory.knot_vectors[0] else: bounds = trajectory.parametric_bounds.ravel() - par_value = _np.linspace(*bounds, nsections) + par_value = _np.linspace(*bounds, nsections + 1) + # compute new knot vector for traj-direction of swept spline + traj_deg = trajectory.degrees[0] + v_dir_kv = _np.empty(traj_deg + len(par_value) + 1) + v_dir_kv[: (traj_deg + 1)] = 0.0 + v_dir_kv[-(traj_deg + 1) :] = 1.0 + v_dir_kv[(traj_deg + 1) : -(traj_deg + 1)] = ( + _np.convolve(par_value.ravel(), _np.ones(traj_deg), "valid")[1:-1] + / traj_deg + ) + par_value = par_value.reshape(-1, 1) # evaluate trajectory at these parameter values @@ -451,16 +421,15 @@ def transformation_matrices(traj, par_value): # evaluate center of cross section and translate to origin cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) cs_center = cross_section.evaluate( - cross_para_center.reshape(-1, 1) + cross_para_center.reshape(-1, cross_section.para_dim) ).ravel() cross_section.control_points -= cs_center # evaluate transformation matrices for every trajectory point A, R = transformation_matrices(trajectory, par_value) - # set cross section evaluation points along trajectory - cps = [] - cross_section_splines = [] + # set cross section control points along trajectory + swept_spline_cps = [] for index, eval_point in enumerate(evals): temp_csp = [] # place every control point of cross section separately @@ -470,52 +439,46 @@ def transformation_matrices(traj, par_value): # transform cross section to global coordinates normal_cscp = _np.matmul(A[index], normal_cscp) # translate cross section to trajectory point - normal_cscp = normal_cscp + eval_point + normal_cscp += eval_point # append control point to list temp_csp.append(normal_cscp) - # create temp spline of new cross section - cross_sec_placed_cps = _np.array(temp_csp) - temp_cross_section = splinepy.BSpline( - degrees=cross_section.degrees, - knot_vectors=cross_section.knot_vectors, - control_points=cross_sec_placed_cps, - ) - - # append cross section spline to list - cross_section_splines.append(temp_cross_section) - cps.append(cross_sec_placed_cps) - - cps = _np.array(cps) - degrees = [int(cross_section.degrees), int(trajectory.degrees)] - knot_vectors = [ - cross_section.knot_vectors[0][:], - trajectory.knot_vectors[0][:], - ] - - fitting_surface = splinepy.BSpline( - degrees, - knot_vectors, - control_points=_np.array(cps).reshape(-1, 3), - ) + # collect all control points + swept_spline_cps.append(_np.array(temp_csp)) - # fit surface - take care of size and n_control_points --> not sure yet - interpolated_surface, _ = surface_from_cross_sections( - fitting_spline=fitting_surface, - cross_section_splines=cross_section_splines, - cps=cps, - size=[ - len(cross_section.control_points), - nsections, + # create spline dictionary + dict_swept_spline = { + "degrees": [*cross_section.degrees, *trajectory.degrees], + "knot_vectors": [ + *cross_section.knot_vectors, + v_dir_kv, ], - n_control_points=[ - len(cross_section.control_points), - len(trajectory.control_points), - ], - # degrees=[int(cross_section.degrees), int(trajectory.degrees)], - ) + "control_points": _np.asarray(swept_spline_cps).reshape( + -1, cross_section.dim + ), + } + + # check if spline is rational + if cross_section.is_rational or trajectory.is_rational: + + def weights(spline): + if spline.is_rational: + return spline.weights + return _np.ones(spline.control_points.shape[0]) + + trajectory_weights = weights(trajectory) + cross_section_weights = weights(cross_section) + dict_swept_spline["weights"] = _np.outer( + trajectory_weights, cross_section_weights + ).reshape(-1, 1) + spline_type = splinepy.NURBS + else: + spline_type = splinepy.BSpline + + # create swept spline + swept_spline = spline_type(**dict_swept_spline) - return interpolated_surface, fitting_surface + return swept_spline def from_bounds(parametric_bounds, physical_bounds): diff --git a/splinepy/helpme/fit.py b/splinepy/helpme/fit.py index 8c6002e97..3450dd96a 100644 --- a/splinepy/helpme/fit.py +++ b/splinepy/helpme/fit.py @@ -589,98 +589,3 @@ def surface( "residual": residual, } return fitted_spline, residual - - -def surface_from_cross_sections( - cross_section_splines, - cps, - size, - # degrees=None, - n_control_points=None, - knot_vectors=None, - fitting_spline=None, - associated_queries=None, - centripetal=True, - interpolate_endpoints=True, - # verbose_output=False, -): - """ - ... - - Parameters - ---------- - ... - Returns - ------- - ... - """ - - # dim = fitting_points.shape[1] - dim = 3 - - # get evaluation locations - u_k = [None, None] - if associated_queries is not None and ( - knot_vectors is not None or fitting_spline is not None - ): - # check dimensions of queries - if ( - associated_queries[0].shape[1] != 1 - or associated_queries[1].shape[1] != 1 - ): - raise ValueError( - "Associated queries in each direction must have dimension 1!" - ) - u_k[0] = _np.asanyarray(associated_queries[0]) - u_k[1] = _np.asanyarray(associated_queries[1]) - - if fitting_spline.para_dim != 2: - raise ValueError( - "If fitting spline is given as one spline, " - "parametric dimension must be 2!" - ) - - # extract spline in each direction (knot_vectors, weights etc. - # of original spline), works for all type of splines! - fitting_splines = fitting_spline.extract.boundaries([2, 0]) - fitted_spline = fitting_spline.copy() - - n_control_points = fitted_spline.control_mesh_resolutions - - # index helper - mi_interim_cps = MultiIndex((n_control_points[0], size[1])) - # loop first dim - # curve fit for every j in n_points_v - interim_control_points = _np.empty((n_control_points[0] * size[1], dim)) - # residual_u = _np.empty(size[1]) - # fitted_spline_u = fitting_splines[0] - for v in range(len(cross_section_splines)): - interim_control_points[mi_interim_cps[:, v]] = cross_section_splines[ - v - ].control_points - - # loop trajectory dim - # curve fit for every k in n_control_points_u - residual_v = _np.empty(n_control_points[0]) - fitted_spline_v = fitting_splines[1] - for u in range(n_control_points[0]): - fitted_spline_v, residual_v[u] = curve( - fitting_points=cps[:, u], - fitting_spline=fitted_spline_v, - # associated_queries=u_k[1], - centripetal=centripetal, - interpolate_endpoints=interpolate_endpoints, - ) - - interim_control_points[mi_interim_cps[u, : n_control_points[1]]] = ( - fitted_spline_v.control_points - ) - - # copy - fitted_spline.control_points = interim_control_points[ - mi_interim_cps[: n_control_points[0], : n_control_points[1]] - ] - - residual = _np.linalg.norm(residual_v) - - return fitted_spline, residual From 5a456958ac619cfdea35aea27ee9fe74de996662 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 19 Jun 2024 15:42:31 +0200 Subject: [PATCH 020/171] Fix: calculation of rotation matrix --- examples/show_swept.py | 12 ++++++------ splinepy/helpme/create.py | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 9a0d9cc55..f55996f95 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -12,10 +12,10 @@ "control_points": np.array( [ [0.0, 0.0, 0.0], - [5.0, 5.0, 0.0], - [10.0, 7.5, 0.0], + [0.0, 0.0, 5.0], [10.0, 5.0, 0.0], - [10.0, 0.0, 0.0], + [15.0, 0.0, -5.0], + [20.0, 0.0, 0.0], ] ), } @@ -24,7 +24,7 @@ trajectory = splinepy.BSpline(**dict_trajectory) # insert knots and control points - # trajectory.uniform_refine([0], 1) + trajectory.uniform_refine([0], 3) ### CROSS SECTION ### dict_cross_section = { @@ -55,7 +55,7 @@ swept_surface = splinepy.helpme.create.swept( trajectory=trajectory, cross_section=cross_section, - nsections=20, + nsections=None, cross_section_normal=cs_nv, ) @@ -68,5 +68,5 @@ ["Trajectory", trajectory], ["Cross Section", cross_section], ["Swept Surface", swept_surface], - resolution=51, + resolution=101, ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 0c28a531e..e890175bf 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -305,7 +305,11 @@ def swept( nsections=None, cross_section_normal=None, ): - """Sweeps a cross-section along a trajectory + """ + Sweeps a cross-section along a trajectory. The cross-section + receives rotation into the direction of the trajectory tangent + vector and is then placed at the evaluation points of the + trajectory's knots. Parameters ---------- @@ -348,10 +352,15 @@ def transformation_matrices(traj, par_value): / _np.linalg.norm(traj.derivative([par_value[0]], [1])) ).ravel() - # evaluating a vector B_0 normal to x + # evaluating a vector normal to x vec = [-x[1], x[0], -x[2]] B = [] - B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) + # avoid dividing by zero + if _np.linalg.norm(_np.cross(x, vec)) > 1e-10: + B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) + else: + vec = [x[2], -x[1], x[0]] + B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) # initialize transformation matrices T = [] @@ -363,7 +372,7 @@ def transformation_matrices(traj, par_value): x = ( traj.derivative([par_value[i]], [1]) / _np.linalg.norm(traj.derivative([par_value[i]], [1])) - ).flatten() + ).ravel() # projecting B_(i-1) onto the plane normal to x B.append(B[i] - _np.dot(B[i], x) * x) @@ -380,20 +389,17 @@ def transformation_matrices(traj, par_value): A.append(_np.linalg.inv(T[i])) # rotation matrix around y - angle_of_x = _np.arctan2(x[2], x[0]) angle_of_cs_normal = _np.arctan2( cross_section_normal[2], cross_section_normal[0] ) - rotation_angle = angle_of_cs_normal - angle_of_x R = _np.array( [ - [_np.cos(rotation_angle), 0, _np.sin(rotation_angle)], + [_np.cos(angle_of_cs_normal), 0, _np.sin(angle_of_cs_normal)], [0, 1, 0], - [_np.sin(rotation_angle), 0, _np.cos(rotation_angle)], + [_np.sin(angle_of_cs_normal), 0, _np.cos(angle_of_cs_normal)], ] ) R = _np.where(_np.abs(R) < 1e-10, 0, R) - return A, R # compute parameter values for inserting cross sections From 6fb1954ec11a06d7001d5edae065690a4e473e66 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 20 Jun 2024 11:44:18 +0200 Subject: [PATCH 021/171] Add: refinement routine; Fix: no more derivative redundance --- examples/show_swept.py | 3 +- splinepy/helpme/create.py | 77 +++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index f55996f95..88a1a775e 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -24,7 +24,7 @@ trajectory = splinepy.BSpline(**dict_trajectory) # insert knots and control points - trajectory.uniform_refine([0], 3) + trajectory.uniform_refine([0], 2) ### CROSS SECTION ### dict_cross_section = { @@ -55,7 +55,6 @@ swept_surface = splinepy.helpme.create.swept( trajectory=trajectory, cross_section=cross_section, - nsections=None, cross_section_normal=cs_nv, ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index e890175bf..ad8a4f9bc 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -302,7 +302,6 @@ def revolved( def swept( cross_section, trajectory, - nsections=None, cross_section_normal=None, ): """ @@ -317,8 +316,6 @@ def swept( Cross-section to be swept trajectory : Spline Trajectory along which the cross-section is swept - nsections : int - Number of sections trajectory is divided into cross_section_normal : np.ndarray Normal vector of the cross-section Default is [0, 0, 1] @@ -345,12 +342,18 @@ def swept( trajectory = trajectory.create.embedded(3) cross_section = cross_section.create.embedded(3) + # print(trajectory.knot_vectors[0]) + # print(len(trajectory.control_points)) + + # # insert knots + # trajectory.insert_knots(0, [0.69, 0.7, 0.1]) + # print(trajectory.knot_vectors[0]) + # print(len(trajectory.control_points)) + def transformation_matrices(traj, par_value): # tangent vector x on trajectory at parameter value 0 - x = ( - traj.derivative([par_value[0]], [1]) - / _np.linalg.norm(traj.derivative([par_value[0]], [1])) - ).ravel() + x = traj.derivative([par_value[0]], [1]) + x = (x / _np.linalg.norm(x)).ravel() # evaluating a vector normal to x vec = [-x[1], x[0], -x[2]] @@ -369,10 +372,8 @@ def transformation_matrices(traj, par_value): # evaluating transformation matrices for each trajectory point for i in range(len(par_value)): # tangent vector x on trajectory at parameter value i - x = ( - traj.derivative([par_value[i]], [1]) - / _np.linalg.norm(traj.derivative([par_value[i]], [1])) - ).ravel() + x = traj.derivative([par_value[i]], [1]) + x = (x / _np.linalg.norm(x)).ravel() # projecting B_(i-1) onto the plane normal to x B.append(B[i] - _np.dot(B[i], x) * x) @@ -402,22 +403,42 @@ def transformation_matrices(traj, par_value): R = _np.where(_np.abs(R) < 1e-10, 0, R) return A, R - # compute parameter values for inserting cross sections - if nsections is None: - par_value = trajectory.greville_abscissae() - v_dir_kv = trajectory.knot_vectors[0] - else: - bounds = trajectory.parametric_bounds.ravel() - par_value = _np.linspace(*bounds, nsections + 1) - # compute new knot vector for traj-direction of swept spline - traj_deg = trajectory.degrees[0] - v_dir_kv = _np.empty(traj_deg + len(par_value) + 1) - v_dir_kv[: (traj_deg + 1)] = 0.0 - v_dir_kv[-(traj_deg + 1) :] = 1.0 - v_dir_kv[(traj_deg + 1) : -(traj_deg + 1)] = ( - _np.convolve(par_value.ravel(), _np.ones(traj_deg), "valid")[1:-1] - / traj_deg - ) + par_value = trajectory.greville_abscissae() + par_value = par_value.reshape(-1, 1) + + curv = [] + for i in par_value: + # calculate derivate of trajectory at parametric value i + curv.append(round(_np.linalg.norm(trajectory.derivative([i], [2])), 2)) + + # insert knots and control points in trajectory area with highest curvature + max_curv = max(curv) + max_indices = [i for i, x in enumerate(curv) if x == max_curv] + par_insertion_values = [] + knot_insertion_values = [] + par_valuesss = par_value.ravel() + p = trajectory.degrees[0] + for i in max_indices: + if i > len(trajectory.knot_vectors[0]) - p - 1: + break + else: + par_insertion_values.append( + (par_valuesss[i] + par_valuesss[i + 1]) / 2 + ) + knot_insertion_values.append( + ( + trajectory.knot_vectors[0][i + p] + + trajectory.knot_vectors[0][i + p + 1] + ) + / 2 + ) + trajectory.insert_knots(0, knot_insertion_values) + par_val_reshape = par_value.reshape(-1) + new_insertion_values_reshape = _np.asanyarray(par_insertion_values) + par_value = _np.concatenate( + (par_val_reshape, new_insertion_values_reshape) + ) + par_value = _np.sort(par_value) par_value = par_value.reshape(-1, 1) @@ -457,7 +478,7 @@ def transformation_matrices(traj, par_value): "degrees": [*cross_section.degrees, *trajectory.degrees], "knot_vectors": [ *cross_section.knot_vectors, - v_dir_kv, + *trajectory.knot_vectors, ], "control_points": _np.asarray(swept_spline_cps).reshape( -1, cross_section.dim From 3b90639b4e9eb1152b3d126578673ec655c5e0ef Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Thu, 27 Jun 2024 14:50:50 +0200 Subject: [PATCH 022/171] Fix: no more global splinepy import --- examples/show_swept.py | 6 +++--- splinepy/helpme/create.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 88a1a775e..b599c1a67 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -22,9 +22,9 @@ # init trajectory as bspline trajectory = splinepy.BSpline(**dict_trajectory) - + trajectory = splinepy.helpme.create.circle(10) # insert knots and control points - trajectory.uniform_refine([0], 2) + trajectory.uniform_refine([0], 4) ### CROSS SECTION ### dict_cross_section = { @@ -45,7 +45,7 @@ cross_section = splinepy.BSpline(**dict_cross_section) # alternatively, use helpme to create a cross section - # cross_section = splinepy.helpme.create.surface_circle(.5).nurbs + cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (default) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index ad8a4f9bc..d6b52c6c3 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -3,7 +3,6 @@ import numpy as _np from gustaf.utils import arr as _arr -import splinepy from splinepy import settings as _settings from splinepy.utils import log as _log from splinepy.utils.data import make_matrix as _make_matrix @@ -326,6 +325,8 @@ def swept( Spline resulting from the sweep """ + from splinepy import NURBS as _NURBS + from splinepy import BSpline as _BSpline from splinepy.spline import Spline as _Spline # check input type @@ -439,7 +440,6 @@ def transformation_matrices(traj, par_value): (par_val_reshape, new_insertion_values_reshape) ) par_value = _np.sort(par_value) - par_value = par_value.reshape(-1, 1) # evaluate trajectory at these parameter values @@ -498,9 +498,9 @@ def weights(spline): dict_swept_spline["weights"] = _np.outer( trajectory_weights, cross_section_weights ).reshape(-1, 1) - spline_type = splinepy.NURBS + spline_type = _NURBS else: - spline_type = splinepy.BSpline + spline_type = _BSpline # create swept spline swept_spline = spline_type(**dict_swept_spline) From 05f226b302ac43fbc78fc5f774e5a9d321f55327 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 1 Jul 2024 12:07:22 +0200 Subject: [PATCH 023/171] Add: self-refinement now working; added function in bspline.py (note: not ideal yet) --- examples/show_swept.py | 6 ++-- splinepy/bspline.py | 58 +++++++++++++++++++++++++++++++++++- splinepy/helpme/create.py | 62 +++++++++++++++++++-------------------- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index b599c1a67..573ad39a6 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -22,9 +22,9 @@ # init trajectory as bspline trajectory = splinepy.BSpline(**dict_trajectory) - trajectory = splinepy.helpme.create.circle(10) + # trajectory = splinepy.helpme.create.circle(10) # insert knots and control points - trajectory.uniform_refine([0], 4) + trajectory.uniform_refine(0, 2) ### CROSS SECTION ### dict_cross_section = { @@ -45,7 +45,7 @@ cross_section = splinepy.BSpline(**dict_cross_section) # alternatively, use helpme to create a cross section - cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs + # cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (default) diff --git a/splinepy/bspline.py b/splinepy/bspline.py index e0d6f7320..a71efe4d2 100644 --- a/splinepy/bspline.py +++ b/splinepy/bspline.py @@ -179,6 +179,63 @@ def determine_new_knots(kv_unique, n_knots): ) self.insert_knots(para_dim, new_knots) + def uniform_refine_fixed_knots(self, para_dims=None, total_knots=4): + """ + Uniformly refines the knot vector in given direction(s) by adding a fixed + total number of knots, which are evenly distributed. + + Parameters + ---------- + para_dims : int or list + list of parametric dimensions to be refined (default None -> all) + total_knots : int + total number of new knots to be added + + Returns + -------- + None + """ + # if no para_dim is given - assume that each dimension is refined + if para_dims is None: + para_dims = range(self.para_dim) + + # if an integer is given, make it a list + elif isinstance(para_dims, int): + para_dims = [para_dims] + + def determine_new_knots_fixed(kv_unique, total_knots): + if total_knots == 0: + return [] + # Calculate the number of new knots to be added between each + # pair of existing knots + num_spans = len(kv_unique) - 1 + new_knots_per_span = total_knots // num_spans + additional_knots = total_knots % num_spans + + new_knots = [] + for i in range(num_spans): + span = kv_unique[i + 1] - kv_unique[i] + num_knots_in_span = new_knots_per_span + ( + 1 if i < additional_knots else 0 + ) + if num_knots_in_span > 0: + new_knots_in_span = kv_unique[i] + span * _np.linspace( + 1 / (num_knots_in_span + 1), + 1 - 1 / (num_knots_in_span + 1), + num_knots_in_span, + ) + new_knots.extend(new_knots_in_span) + return _np.array(new_knots) + + # determine new knots for each para_dim and insert the knots + for para_dim in para_dims: + new_knots = determine_new_knots_fixed( + # recompute unique to allow duplicating para_dims. + kv_unique=self.unique_knots[para_dim], + total_knots=total_knots, + ) + self.insert_knots(para_dim, new_knots) + def knot_insertion_matrix( self, parametric_dimension=None, knots=None, beziers=False ): @@ -319,7 +376,6 @@ def remove_knots(self, parametric_dimension, knots, tolerance=None): self._logd(f"Tried to remove {len(knots)} knot(s).") self._logd(f"Actually removed {sum(removed)} knot(s).") - return removed def normalize_knot_vectors(self): diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index d6b52c6c3..9a66502a9 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -311,7 +311,7 @@ def swept( Parameters ---------- - crossection : Spline + cross_section : Spline Cross-section to be swept trajectory : Spline Trajectory along which the cross-section is swept @@ -343,14 +343,6 @@ def swept( trajectory = trajectory.create.embedded(3) cross_section = cross_section.create.embedded(3) - # print(trajectory.knot_vectors[0]) - # print(len(trajectory.control_points)) - - # # insert knots - # trajectory.insert_knots(0, [0.69, 0.7, 0.1]) - # print(trajectory.knot_vectors[0]) - # print(len(trajectory.control_points)) - def transformation_matrices(traj, par_value): # tangent vector x on trajectory at parameter value 0 x = traj.derivative([par_value[0]], [1]) @@ -407,42 +399,48 @@ def transformation_matrices(traj, par_value): par_value = trajectory.greville_abscissae() par_value = par_value.reshape(-1, 1) + ### insert knots in trajectory area with highest curvature ### curv = [] for i in par_value: - # calculate derivate of trajectory at parametric value i + # calculate curvature of trajectory at parametric value i curv.append(round(_np.linalg.norm(trajectory.derivative([i], [2])), 2)) - - # insert knots and control points in trajectory area with highest curvature + # evaluate the par_values-vector indices of the maximum curvature points max_curv = max(curv) max_indices = [i for i, x in enumerate(curv) if x == max_curv] - par_insertion_values = [] - knot_insertion_values = [] - par_valuesss = par_value.ravel() - p = trajectory.degrees[0] - for i in max_indices: - if i > len(trajectory.knot_vectors[0]) - p - 1: - break + # prepare two matrices for the insertion + insertion_values = [] + # compute the new insertion values + par_values = par_value.ravel() + + for maxi in max_indices: + if maxi == 0: + insertion_values.append( + (par_values[maxi] + par_values[maxi + 1]) / 2 + ) + elif maxi == len(par_values) - 1: + insertion_values.append( + (par_values[maxi] + par_values[maxi - 1]) / 2 + ) else: - par_insertion_values.append( - (par_valuesss[i] + par_valuesss[i + 1]) / 2 + insertion_values.append( + (par_values[maxi] + par_values[maxi - 1]) / 2 ) - knot_insertion_values.append( - ( - trajectory.knot_vectors[0][i + p] - + trajectory.knot_vectors[0][i + p + 1] - ) - / 2 + insertion_values.append( + (par_values[maxi] + par_values[maxi + 1]) / 2 ) - trajectory.insert_knots(0, knot_insertion_values) - par_val_reshape = par_value.reshape(-1) - new_insertion_values_reshape = _np.asanyarray(par_insertion_values) + + # insert knots into the trajectory's knot vector + insertion_values = _np.unique(insertion_values) + trajectory.uniform_refine_fixed_knots(0, len(insertion_values)) + # insert knots into the parameter values par_value = _np.concatenate( - (par_val_reshape, new_insertion_values_reshape) + (par_value.reshape(-1), _np.asanyarray(insertion_values)) ) + # sort parameter values par_value = _np.sort(par_value) par_value = par_value.reshape(-1, 1) - # evaluate trajectory at these parameter values + # evaluate trajectory at the parameter values evals = trajectory.evaluate(par_value) # evaluate center of cross section and translate to origin From 3c3a80eff881af9b7d6feca51aba5cc834837cf5 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 2 Jul 2024 10:58:59 +0200 Subject: [PATCH 024/171] Add: function for closed trajectories --- examples/show_swept.py | 131 +++++++++++++++++++++++++++++++++++--- splinepy/helpme/create.py | 62 ++++++++++++++++-- 2 files changed, 179 insertions(+), 14 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 573ad39a6..9e3a82e50 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -1,3 +1,5 @@ +import sys + import gustaf as gus import numpy as np @@ -6,25 +8,87 @@ if __name__ == "__main__": ### TRAJECTORY ### + + # arbitrary trajectory + # dict_trajectory = { + # "degrees": [2], + # "knot_vectors": [[0.0, 0.0, 0.0, 0.333, 0.666, 1.0, 1.0, 1.0]], + # "control_points": np.array( + # [ + # [0.0, 0.0, 0.0], + # [0.0, 0.0, 5.0], + # [10.0, 5.0, 0.0], + # [15.0, 0.0, -5.0], + # [20.0, 0.0, 0.0], + # ] + # ), + # } + + # 2D questionmark + # dict_trajectory = { + # "degrees": [3], + # "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.2, 0.4, + # 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0]], + # "control_points": np.array([ + # [0.5, 0], # Startpunkt + # [0.5, 2], + # [1.0, 3], + # [2.0, 4], + # [2.15, 5], + # [1.8, 5.9], + # [1.0, 6.2], + # [-0.25, 6], + # [-0.5, 5],]) + # } + # init trajectory as bspline + + # closed 3D questionmark dict_trajectory = { - "degrees": [2], - "knot_vectors": [[0.0, 0.0, 0.0, 0.333, 0.666, 1.0, 1.0, 1.0]], + "degrees": [3], + "knot_vectors": [ + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.8, + 0.9, + 1.0, + 1.0, + 1.0, + 1.0, + ] + ], "control_points": np.array( [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 5.0], - [10.0, 5.0, 0.0], - [15.0, 0.0, -5.0], - [20.0, 0.0, 0.0], + [0.5, 0, 0], + [0.5, 2, 0.3], + [1.0, 3, 0.1], + [2.0, 4, -0.1], + [2.15, 5, -0.2], + [1.8, 5.9, -0.4], + [1.0, 6.2, -0.3], + [-0.25, 6, -0.1], + [-0.5, 5.0, 0.1], + [-2.0, 4.0, 0.2], + [-1, 3.0, 0.1], + [0.5, 0.0, 0.0], ] ), } - - # init trajectory as bspline trajectory = splinepy.BSpline(**dict_trajectory) + + # alternatively, use helpme to create a trajectory # trajectory = splinepy.helpme.create.circle(10) + # insert knots and control points - trajectory.uniform_refine(0, 2) + trajectory.uniform_refine(0, 1) ### CROSS SECTION ### dict_cross_section = { @@ -69,3 +133,50 @@ ["Swept Surface", swept_surface], resolution=101, ) + + sys.exit() + + ### EXPORT A SWEPT SPLINE ### + dict_export_cs = { + "degrees": [1], + "knot_vectors": [[0.0, 0.0, 1.0, 1.0]], + "control_points": np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + ] + ), + } + export_cs = splinepy.BSpline(**dict_export_cs) + + dict_export_traj = { + "degrees": [3], + "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], + "control_points": np.array( + [ + [0.0, 0.0], + [1.0, 2.0], + [2.0, 0.0], + [3.0, -2.0], + [4.0, 0.0], + ] + ), + } + export_traj = splinepy.BSpline(**dict_export_traj) + + swept_surface = splinepy.helpme.create.swept( + trajectory=export_traj, + cross_section=export_cs, + cross_section_normal=[-1, 0, 0], + ) + + gus.show( + ["Trajectory", export_traj], + ["Cross Section", export_cs], + ["Swept Surface", swept_surface], + resolution=101, + ) + + projection = swept_surface.create.embedded(2) + gus.show(["Projection", projection], resolution=101) + splinepy.io.mfem.export("testmeshmesh.mesh", projection) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 9a66502a9..f17001788 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -308,6 +308,8 @@ def swept( receives rotation into the direction of the trajectory tangent vector and is then placed at the evaluation points of the trajectory's knots. + Fundamental ideas can be found in the NURBS Book, Piegl & Tiller, + 2nd edition, chapter 10.4 Swept Surfaces. Parameters ---------- @@ -315,7 +317,7 @@ def swept( Cross-section to be swept trajectory : Spline Trajectory along which the cross-section is swept - cross_section_normal : np.ndarray + cross_section_normal : np.array Normal vector of the cross-section Default is [0, 0, 1] @@ -344,6 +346,9 @@ def swept( cross_section = cross_section.create.embedded(3) def transformation_matrices(traj, par_value): + + ### TRANSFORMATION MATRICES ### + # tangent vector x on trajectory at parameter value 0 x = traj.derivative([par_value[0]], [1]) x = (x / _np.linalg.norm(x)).ravel() @@ -358,17 +363,19 @@ def transformation_matrices(traj, par_value): vec = [x[2], -x[1], x[0]] B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) - # initialize transformation matrices + # initialize transformation matrices and x-collection T = [] A = [] + x_collection = [] # evaluating transformation matrices for each trajectory point for i in range(len(par_value)): # tangent vector x on trajectory at parameter value i x = traj.derivative([par_value[i]], [1]) x = (x / _np.linalg.norm(x)).ravel() + x_collection.append(x) - # projecting B_(i-1) onto the plane normal to x + # projecting B_(i) onto the plane normal to x B.append(B[i] - _np.dot(B[i], x) * x) B[i + 1] = B[i + 1] / _np.linalg.norm(B[i + 1]) @@ -382,7 +389,49 @@ def transformation_matrices(traj, par_value): # array of transformation matrices from local to global coordinates A.append(_np.linalg.inv(T[i])) - # rotation matrix around y + # own procedure, if trajectory is closed and B[0] != B[-1] + # according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483 + if _np.array_equal( + traj.evaluate([[0]]), traj.evaluate([par_value[-1]]) + ) and not _np.array_equal(B[0], B[-1]): + # reset transformation matrices + T = [] + A = [] + B_reverse = [None] * len(B) + B_reverse[0] = B[-1] + for i in range(len(par_value)): + # redo the calculation of B using x_collection from before + B_reverse[i + 1] = ( + B_reverse[i] + - _np.dot(B_reverse[i], x_collection[i]) * x_collection[i] + ) + B_reverse[i + 1] = B_reverse[i + 1] / _np.linalg.norm( + B_reverse[i + 1] + ) + # middle point between B and B_reverse + B_reverse[i + 1] = (B[i + 1] + B_reverse[i + 1]) * 0.5 + B_reverse[i + 1] = B_reverse[i + 1] / _np.linalg.norm( + B_reverse[i + 1] + ) + # defining y and z axis-vectors + z = B_reverse[i + 1] + y = _np.cross(z, x_collection[i]) + + # array of transformation matrices from global to local coordinates + T.append(_np.vstack((x_collection[i], y, z))) + + # array of transformation matrices from local to global coordinates + A.append(_np.linalg.inv(T[i])) + + # check if the beginning and the end of the B-vector are the same + if not _np.allclose(B_reverse[0], B_reverse[-1]): + _log.warning( + "Vector calculation is not exact due to the " + "trajectory being closed in an uncommon way." + ) + + ### ROTATION MATRIX AROUND Y ### + angle_of_cs_normal = _np.arctan2( cross_section_normal[2], cross_section_normal[0] ) @@ -394,8 +443,11 @@ def transformation_matrices(traj, par_value): ] ) R = _np.where(_np.abs(R) < 1e-10, 0, R) + return A, R + ### REFINEMENT OF TRAJECTORY ### + par_value = trajectory.greville_abscissae() par_value = par_value.reshape(-1, 1) @@ -440,6 +492,8 @@ def transformation_matrices(traj, par_value): par_value = _np.sort(par_value) par_value = par_value.reshape(-1, 1) + ### SWEEPING PROCESS ### + # evaluate trajectory at the parameter values evals = trajectory.evaluate(par_value) From 37083cd0184d12e238559de457ea75f1a1dfc73e Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 2 Jul 2024 13:18:55 +0200 Subject: [PATCH 025/171] Add: some checks were added --- splinepy/helpme/create.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index f17001788..912aa6779 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -336,6 +336,10 @@ def swept( raise NotImplementedError("Sweep only works for splines") if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweep only works for splines") + if not trajectory.para_dim == 1: + raise NotImplementedError("Trajectory must be 1D") + if not len(cross_section_normal) == 3: + raise ValueError("Cross section normal must be 3D") # setting default value for cross_section_normal if cross_section_normal is None: @@ -459,7 +463,7 @@ def transformation_matrices(traj, par_value): # evaluate the par_values-vector indices of the maximum curvature points max_curv = max(curv) max_indices = [i for i, x in enumerate(curv) if x == max_curv] - # prepare two matrices for the insertion + # prepare matrix for the insertion insertion_values = [] # compute the new insertion values par_values = par_value.ravel() @@ -484,7 +488,7 @@ def transformation_matrices(traj, par_value): # insert knots into the trajectory's knot vector insertion_values = _np.unique(insertion_values) trajectory.uniform_refine_fixed_knots(0, len(insertion_values)) - # insert knots into the parameter values + # add insertion values to the existing parameter values par_value = _np.concatenate( (par_value.reshape(-1), _np.asanyarray(insertion_values)) ) @@ -551,6 +555,7 @@ def weights(spline): trajectory_weights, cross_section_weights ).reshape(-1, 1) spline_type = _NURBS + else: spline_type = _BSpline From 3d233d380f845016af8eb1e032133b2c8b26fc1f Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 2 Jul 2024 13:23:48 +0200 Subject: [PATCH 026/171] Add: reference to nurbs book --- splinepy/helpme/create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 912aa6779..2f90750c5 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -374,6 +374,7 @@ def transformation_matrices(traj, par_value): # evaluating transformation matrices for each trajectory point for i in range(len(par_value)): + # calculation according to NURBS Book, eq. 10.27 # tangent vector x on trajectory at parameter value i x = traj.derivative([par_value[i]], [1]) x = (x / _np.linalg.norm(x)).ravel() @@ -405,6 +406,7 @@ def transformation_matrices(traj, par_value): B_reverse[0] = B[-1] for i in range(len(par_value)): # redo the calculation of B using x_collection from before + # according to NURBS Book, eq. 10.27 B_reverse[i + 1] = ( B_reverse[i] - _np.dot(B_reverse[i], x_collection[i]) * x_collection[i] From 37f180d4327eeb60092e76033c996f62c1b2d1ac Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 2 Jul 2024 13:36:38 +0200 Subject: [PATCH 027/171] Add: auto_refinement variable --- examples/show_swept.py | 1 + splinepy/helpme/create.py | 91 ++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 9e3a82e50..e4007169e 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -120,6 +120,7 @@ trajectory=trajectory, cross_section=cross_section, cross_section_normal=cs_nv, + auto_refinement=True, ) ### VISUALIZATION ### diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 2f90750c5..ed0fa2639 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -302,6 +302,7 @@ def swept( cross_section, trajectory, cross_section_normal=None, + auto_refinement=False, ): """ Sweeps a cross-section along a trajectory. The cross-section @@ -320,6 +321,9 @@ def swept( cross_section_normal : np.array Normal vector of the cross-section Default is [0, 0, 1] + auto_refinement : bool + If True, the trajectory will be refined at points of + highest curvature. Default is False. Returns ------- @@ -340,6 +344,8 @@ def swept( raise NotImplementedError("Trajectory must be 1D") if not len(cross_section_normal) == 3: raise ValueError("Cross section normal must be 3D") + if not isinstance(auto_refinement, bool): + raise ValueError("auto_refinement must be a boolean") # setting default value for cross_section_normal if cross_section_normal is None: @@ -349,6 +355,10 @@ def swept( trajectory = trajectory.create.embedded(3) cross_section = cross_section.create.embedded(3) + # initialize parameter values + par_value = trajectory.greville_abscissae() + par_value = par_value.reshape(-1, 1) + def transformation_matrices(traj, par_value): ### TRANSFORMATION MATRICES ### @@ -454,49 +464,50 @@ def transformation_matrices(traj, par_value): ### REFINEMENT OF TRAJECTORY ### - par_value = trajectory.greville_abscissae() - par_value = par_value.reshape(-1, 1) + if auto_refinement: + ## inserts knots in trajectory area with highest curvature # - ### insert knots in trajectory area with highest curvature ### - curv = [] - for i in par_value: - # calculate curvature of trajectory at parametric value i - curv.append(round(_np.linalg.norm(trajectory.derivative([i], [2])), 2)) - # evaluate the par_values-vector indices of the maximum curvature points - max_curv = max(curv) - max_indices = [i for i, x in enumerate(curv) if x == max_curv] - # prepare matrix for the insertion - insertion_values = [] - # compute the new insertion values - par_values = par_value.ravel() - - for maxi in max_indices: - if maxi == 0: - insertion_values.append( - (par_values[maxi] + par_values[maxi + 1]) / 2 - ) - elif maxi == len(par_values) - 1: - insertion_values.append( - (par_values[maxi] + par_values[maxi - 1]) / 2 - ) - else: - insertion_values.append( - (par_values[maxi] + par_values[maxi - 1]) / 2 - ) - insertion_values.append( - (par_values[maxi] + par_values[maxi + 1]) / 2 + curv = [] + for i in par_value: + # calculate curvature of trajectory at parametric value i + curv.append( + round(_np.linalg.norm(trajectory.derivative([i], [2])), 2) ) + # evaluate the par_values-vector indices of the maximum curvature points + max_curv = max(curv) + max_indices = [i for i, x in enumerate(curv) if x == max_curv] + # prepare matrix for the insertion + insertion_values = [] + # compute the new insertion values + par_values = par_value.ravel() + + for maxi in max_indices: + if maxi == 0: + insertion_values.append( + (par_values[maxi] + par_values[maxi + 1]) / 2 + ) + elif maxi == len(par_values) - 1: + insertion_values.append( + (par_values[maxi] + par_values[maxi - 1]) / 2 + ) + else: + insertion_values.append( + (par_values[maxi] + par_values[maxi - 1]) / 2 + ) + insertion_values.append( + (par_values[maxi] + par_values[maxi + 1]) / 2 + ) - # insert knots into the trajectory's knot vector - insertion_values = _np.unique(insertion_values) - trajectory.uniform_refine_fixed_knots(0, len(insertion_values)) - # add insertion values to the existing parameter values - par_value = _np.concatenate( - (par_value.reshape(-1), _np.asanyarray(insertion_values)) - ) - # sort parameter values - par_value = _np.sort(par_value) - par_value = par_value.reshape(-1, 1) + # insert knots into the trajectory's knot vector + insertion_values = _np.unique(insertion_values) + trajectory.uniform_refine_fixed_knots(0, len(insertion_values)) + # add insertion values to the existing parameter values + par_value = _np.concatenate( + (par_value.reshape(-1), _np.asanyarray(insertion_values)) + ) + # sort parameter values + par_value = _np.sort(par_value) + par_value = par_value.reshape(-1, 1) ### SWEEPING PROCESS ### From bf2b96196e83686eaf0e5ef36e16d2f1277d0c7e Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 2 Jul 2024 17:23:14 +0200 Subject: [PATCH 028/171] Fix: positioning of cs-cps now acc to traj-cps; Fix: auto_refinement routine --- examples/show_swept.py | 132 ++++++++++++++++++++------------------ splinepy/helpme/create.py | 45 +++++++++---- 2 files changed, 100 insertions(+), 77 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index e4007169e..ba22802a1 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -25,63 +25,67 @@ # } # 2D questionmark - # dict_trajectory = { - # "degrees": [3], - # "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.2, 0.4, - # 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0]], - # "control_points": np.array([ - # [0.5, 0], # Startpunkt - # [0.5, 2], - # [1.0, 3], - # [2.0, 4], - # [2.15, 5], - # [1.8, 5.9], - # [1.0, 6.2], - # [-0.25, 6], - # [-0.5, 5],]) - # } - # init trajectory as bspline - - # closed 3D questionmark dict_trajectory = { "degrees": [3], "knot_vectors": [ - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.1, - 0.2, - 0.3, - 0.4, - 0.5, - 0.6, - 0.8, - 0.9, - 1.0, - 1.0, - 1.0, - 1.0, - ] + [0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0] ], "control_points": np.array( [ - [0.5, 0, 0], - [0.5, 2, 0.3], - [1.0, 3, 0.1], - [2.0, 4, -0.1], - [2.15, 5, -0.2], - [1.8, 5.9, -0.4], - [1.0, 6.2, -0.3], - [-0.25, 6, -0.1], - [-0.5, 5.0, 0.1], - [-2.0, 4.0, 0.2], - [-1, 3.0, 0.1], - [0.5, 0.0, 0.0], + [0.5, 0], # Startpunkt + [0.5, 2], + [1.0, 3], + [2.0, 4], + [2.15, 5], + [1.8, 5.9], + [1.0, 6.2], + [-0.25, 6], + [-0.5, 5], ] ), } + # init trajectory as bspline + + # closed 3D questionmark + # dict_trajectory = { + # "degrees": [3], + # "knot_vectors": [ + # [ + # 0.0, + # 0.0, + # 0.0, + # 0.0, + # 0.1, + # 0.2, + # 0.3, + # 0.4, + # 0.5, + # 0.6, + # 0.8, + # 0.9, + # 1.0, + # 1.0, + # 1.0, + # 1.0, + # ] + # ], + # "control_points": np.array( + # [ + # [0.5, 0, 0], + # [0.5, 2, 0.3], + # [1.0, 3, 0.1], + # [2.0, 4, -0.1], + # [2.15, 5, -0.2], + # [1.8, 5.9, -0.4], + # [1.0, 6.2, -0.3], + # [-0.25, 6, -0.1], + # [-0.5, 5.0, 0.1], + # [-2.0, 4.0, 0.2], + # [-1, 3.0, 0.1], + # [0.5, 0.0, 0.0], + # ] + # ), + # } trajectory = splinepy.BSpline(**dict_trajectory) # alternatively, use helpme to create a trajectory @@ -91,25 +95,25 @@ trajectory.uniform_refine(0, 1) ### CROSS SECTION ### - dict_cross_section = { - "degrees": [3], - "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], - "control_points": np.array( - [ - [0.0, 0.0, 0.0], - [1.0, 2.0, 0.0], - [2.0, 0.0, 0.0], - [3.0, -2.0, 0.0], - [4.0, 0.0, 0.0], - ] - ), - } + # dict_cross_section = { + # "degrees": [3], + # "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], + # "control_points": np.array( + # [ + # [0.0, 0.0, 0.0], + # [1.0, 2.0, 0.0], + # [2.0, 0.0, 0.0], + # [3.0, -2.0, 0.0], + # [4.0, 0.0, 0.0], + # ] + # ), + # } - # init cross section as bspline - cross_section = splinepy.BSpline(**dict_cross_section) + # # init cross section as bspline + # cross_section = splinepy.BSpline(**dict_cross_section) # alternatively, use helpme to create a cross section - # cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs + cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (default) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index ed0fa2639..45b50ec68 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -440,7 +440,7 @@ def transformation_matrices(traj, par_value): A.append(_np.linalg.inv(T[i])) # check if the beginning and the end of the B-vector are the same - if not _np.allclose(B_reverse[0], B_reverse[-1]): + if not _np.allclose(B_reverse[0], B_reverse[-1], rtol=1e-3): _log.warning( "Vector calculation is not exact due to the " "trajectory being closed in an uncommon way." @@ -500,20 +500,39 @@ def transformation_matrices(traj, par_value): # insert knots into the trajectory's knot vector insertion_values = _np.unique(insertion_values) - trajectory.uniform_refine_fixed_knots(0, len(insertion_values)) - # add insertion values to the existing parameter values - par_value = _np.concatenate( - (par_value.reshape(-1), _np.asanyarray(insertion_values)) - ) - # sort parameter values - par_value = _np.sort(par_value) + # convert knot vector to list + kv_list = trajectory.knot_vectors[0].numpy() + + # insert knots into the trajectory's knot vector + if any(value in insertion_values for value in kv_list): + add = _np.concatenate((insertion_values, kv_list)) + add = _np.unique(add) + # remove the existing knots + add = add[~_np.isin(add, kv_list)] + # check if there are any knots to insert + if len(add) == 0: + _log.warning("Auto Refinement couldn't insert knots.") + else: + trajectory.insert_knots(0, add) + # give information about the inserted knots + _log.info( + f"Auto Refinement inserted {len(add)} " + "knots into the trajectory." + ) + else: + trajectory.insert_knots(0, insertion_values) + # give information about the inserted knots + _log.info( + f"Auto Refinement inserted {len(insertion_values)} " + "knots into the trajectory." + ) + + # recalculate parameter values + par_value = trajectory.greville_abscissae() par_value = par_value.reshape(-1, 1) ### SWEEPING PROCESS ### - # evaluate trajectory at the parameter values - evals = trajectory.evaluate(par_value) - # evaluate center of cross section and translate to origin cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) cs_center = cross_section.evaluate( @@ -526,7 +545,7 @@ def transformation_matrices(traj, par_value): # set cross section control points along trajectory swept_spline_cps = [] - for index, eval_point in enumerate(evals): + for index in range(len(par_value)): temp_csp = [] # place every control point of cross section separately for cscp in cross_section.control_points: @@ -535,7 +554,7 @@ def transformation_matrices(traj, par_value): # transform cross section to global coordinates normal_cscp = _np.matmul(A[index], normal_cscp) # translate cross section to trajectory point - normal_cscp += eval_point + normal_cscp += trajectory.control_points[index] # append control point to list temp_csp.append(normal_cscp) From 8930b19540a0818379475db66ead80ece4b3bfaa Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 2 Jul 2024 17:32:50 +0200 Subject: [PATCH 029/171] Add: test functions in create-test-file --- splinepy/helpme/create.py | 3 +- tests/helpme/test_create.py | 76 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 45b50ec68..f2a11d547 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -342,7 +342,8 @@ def swept( raise NotImplementedError("Sweep only works for splines") if not trajectory.para_dim == 1: raise NotImplementedError("Trajectory must be 1D") - if not len(cross_section_normal) == 3: + + if cross_section_normal is not None and not len(cross_section_normal) == 3: raise ValueError("Cross section normal must be 3D") if not isinstance(auto_refinement, bool): raise ValueError("auto_refinement must be a boolean") diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index 265f3c103..1c79c7b23 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -343,3 +343,79 @@ def test_determinant_spline( det_spl.evaluate(queries=rnd_queries).ravel(), np.linalg.det(sp_i.jacobian(queries=rnd_queries)), ), f"{sp_i.whatami} at index {idx} failed determinant spline" + + +def test_swept_basic_functionality(): + cross_section = splinepy.BSpline( + degrees=[2], + control_points=[[0, 0], [0.5, 1], [1, 0]], + knot_vectors=[[0, 0, 0, 1, 1, 1]], + ) + trajectory = splinepy.BSpline( + degrees=[3], + control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], + knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], + ) + result = splinepy.helpme.create.swept(cross_section, trajectory) + assert result is not None + assert ( + result.control_points.shape[0] + == cross_section.control_points.shape[0] + * trajectory.control_points.shape[0] + ) + + +def test_swept_with_custom_normal(): + cross_section = splinepy.BSpline( + degrees=[2], + control_points=[[0, 0], [0.5, 1], [1, 0]], + knot_vectors=[[0, 0, 0, 1, 1, 1]], + ) + trajectory = splinepy.BSpline( + degrees=[3], + control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], + knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], + ) + custom_normal = np.array([0, 1, 0]) + result = splinepy.helpme.create.swept( + cross_section, trajectory, cross_section_normal=custom_normal + ) + assert result is not None + + +def test_swept_invalid_inputs(): + cross_section = splinepy.BSpline( + degrees=[2], + control_points=[[0, 0], [0.5, 1], [1, 0]], + knot_vectors=[[0, 0, 0, 1, 1, 1]], + ) + invalid_trajectory = "invalid_trajectory" + with pytest.raises(NotImplementedError): + splinepy.helpme.create.swept(cross_section, invalid_trajectory) + + invalid_cross_section = "invalid_cross_section" + trajectory = splinepy.BSpline( + degrees=[3], + control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], + knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], + ) + with pytest.raises(NotImplementedError): + splinepy.helpme.create.swept(invalid_cross_section, trajectory) + + +def test_swept_rational_splines(): + cross_section = splinepy.NURBS( + degrees=[2], + control_points=[[0, 0], [0.5, 1], [1, 0]], + weights=[1, 0.5, 1], + knot_vectors=[[0, 0, 0, 1, 1, 1]], + ) + trajectory = splinepy.NURBS( + degrees=[3], + control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], + weights=[1, 0.5, 0.5, 1], + knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], + ) + result = splinepy.helpme.create.swept(cross_section, trajectory) + assert result is not None + assert result.is_rational From 8c2cc7f219781f34504ce857eff7ce54937fd4a3 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 10:18:15 +0200 Subject: [PATCH 030/171] Rm: removed unnecessary insertion function in bspline.py --- splinepy/bspline.py | 57 --------------------------------------------- 1 file changed, 57 deletions(-) diff --git a/splinepy/bspline.py b/splinepy/bspline.py index a71efe4d2..b932b27df 100644 --- a/splinepy/bspline.py +++ b/splinepy/bspline.py @@ -179,63 +179,6 @@ def determine_new_knots(kv_unique, n_knots): ) self.insert_knots(para_dim, new_knots) - def uniform_refine_fixed_knots(self, para_dims=None, total_knots=4): - """ - Uniformly refines the knot vector in given direction(s) by adding a fixed - total number of knots, which are evenly distributed. - - Parameters - ---------- - para_dims : int or list - list of parametric dimensions to be refined (default None -> all) - total_knots : int - total number of new knots to be added - - Returns - -------- - None - """ - # if no para_dim is given - assume that each dimension is refined - if para_dims is None: - para_dims = range(self.para_dim) - - # if an integer is given, make it a list - elif isinstance(para_dims, int): - para_dims = [para_dims] - - def determine_new_knots_fixed(kv_unique, total_knots): - if total_knots == 0: - return [] - # Calculate the number of new knots to be added between each - # pair of existing knots - num_spans = len(kv_unique) - 1 - new_knots_per_span = total_knots // num_spans - additional_knots = total_knots % num_spans - - new_knots = [] - for i in range(num_spans): - span = kv_unique[i + 1] - kv_unique[i] - num_knots_in_span = new_knots_per_span + ( - 1 if i < additional_knots else 0 - ) - if num_knots_in_span > 0: - new_knots_in_span = kv_unique[i] + span * _np.linspace( - 1 / (num_knots_in_span + 1), - 1 - 1 / (num_knots_in_span + 1), - num_knots_in_span, - ) - new_knots.extend(new_knots_in_span) - return _np.array(new_knots) - - # determine new knots for each para_dim and insert the knots - for para_dim in para_dims: - new_knots = determine_new_knots_fixed( - # recompute unique to allow duplicating para_dims. - kv_unique=self.unique_knots[para_dim], - total_knots=total_knots, - ) - self.insert_knots(para_dim, new_knots) - def knot_insertion_matrix( self, parametric_dimension=None, knots=None, beziers=False ): From f610564d5661ddd1c8d5b5a6db08eeee9b89e7a7 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 11:03:23 +0200 Subject: [PATCH 031/171] Add: opportunity to set cross-sec CPs on traj eval points or traj CPs --- examples/show_swept.py | 105 +++++++++++++++++++------------------- splinepy/helpme/create.py | 24 +++++++-- 2 files changed, 72 insertions(+), 57 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index ba22802a1..003899b89 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -25,67 +25,67 @@ # } # 2D questionmark - dict_trajectory = { - "degrees": [3], - "knot_vectors": [ - [0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0] - ], - "control_points": np.array( - [ - [0.5, 0], # Startpunkt - [0.5, 2], - [1.0, 3], - [2.0, 4], - [2.15, 5], - [1.8, 5.9], - [1.0, 6.2], - [-0.25, 6], - [-0.5, 5], - ] - ), - } - # init trajectory as bspline - - # closed 3D questionmark # dict_trajectory = { # "degrees": [3], # "knot_vectors": [ - # [ - # 0.0, - # 0.0, - # 0.0, - # 0.0, - # 0.1, - # 0.2, - # 0.3, - # 0.4, - # 0.5, - # 0.6, - # 0.8, - # 0.9, - # 1.0, - # 1.0, - # 1.0, - # 1.0, - # ] + # [0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0] # ], # "control_points": np.array( # [ - # [0.5, 0, 0], - # [0.5, 2, 0.3], - # [1.0, 3, 0.1], - # [2.0, 4, -0.1], - # [2.15, 5, -0.2], - # [1.8, 5.9, -0.4], - # [1.0, 6.2, -0.3], - # [-0.25, 6, -0.1], - # [-0.5, 5.0, 0.1], - # [-2.0, 4.0, 0.2], - # [-1, 3.0, 0.1], - # [0.5, 0.0, 0.0], + # [0.5, 0], # Startpunkt + # [0.5, 2], + # [1.0, 3], + # [2.0, 4], + # [2.15, 5], + # [1.8, 5.9], + # [1.0, 6.2], + # [-0.25, 6], + # [-0.5, 5], # ] # ), # } + # init trajectory as bspline + + # closed 3D questionmark + dict_trajectory = { + "degrees": [3], + "knot_vectors": [ + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.8, + 0.9, + 1.0, + 1.0, + 1.0, + 1.0, + ] + ], + "control_points": np.array( + [ + [0.5, 0, 0], + [0.5, 2, 0.3], + [1.0, 3, 0.1], + [2.0, 4, -0.1], + [2.15, 5, -0.2], + [1.8, 5.9, -0.4], + [1.0, 6.2, -0.3], + [-0.25, 6, -0.1], + [-0.5, 5.0, 0.1], + [-2.0, 4.0, 0.2], + [-1, 3.0, 0.1], + [0.5, 0.0, 0.0], + ] + ), + } trajectory = splinepy.BSpline(**dict_trajectory) # alternatively, use helpme to create a trajectory @@ -125,6 +125,7 @@ cross_section=cross_section, cross_section_normal=cs_nv, auto_refinement=True, + set_on_trajectory=True, ) ### VISUALIZATION ### diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index f2a11d547..0eca124bf 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -303,6 +303,7 @@ def swept( trajectory, cross_section_normal=None, auto_refinement=False, + set_on_trajectory=False, ): """ Sweeps a cross-section along a trajectory. The cross-section @@ -324,6 +325,11 @@ def swept( auto_refinement : bool If True, the trajectory will be refined at points of highest curvature. Default is False. + set_on_trajectory : bool + If True, the cross-section will be placed at the evaluation + points of the trajectory's knots. If False, the cross-section + will be placed at the control points of the trajectory. + Default is False. Returns ------- @@ -466,7 +472,7 @@ def transformation_matrices(traj, par_value): ### REFINEMENT OF TRAJECTORY ### if auto_refinement: - ## inserts knots in trajectory area with highest curvature # + ## inserts knots in trajectory area with highest curvature ## curv = [] for i in par_value: @@ -546,16 +552,24 @@ def transformation_matrices(traj, par_value): # set cross section control points along trajectory swept_spline_cps = [] - for index in range(len(par_value)): + for i, par_val in enumerate(par_value): temp_csp = [] + if set_on_trajectory: + evals = trajectory.evaluate([par_val]).ravel() # place every control point of cross section separately for cscp in cross_section.control_points: # rotate cross section in trajectory direction normal_cscp = _np.matmul(R, cscp) # transform cross section to global coordinates - normal_cscp = _np.matmul(A[index], normal_cscp) - # translate cross section to trajectory point - normal_cscp += trajectory.control_points[index] + normal_cscp = _np.matmul(A[i], normal_cscp) + # check whether user wants to place cross section + # at evaluation points or control points of trajectory + if set_on_trajectory: + # translate cross section to trajectory evaluation point + normal_cscp += evals + else: + # translate cross section to trajectory control point + normal_cscp += trajectory.control_points[i] # append control point to list temp_csp.append(normal_cscp) From 8f1142d1d45b07b8545acfac3694f7d5b43c48e0 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 11:38:22 +0200 Subject: [PATCH 032/171] Add: swept description; Fix: using settings.TOLERANCE --- examples/show_swept.py | 144 +++++++++++++++++++------------------- splinepy/helpme/create.py | 15 ++-- 2 files changed, 82 insertions(+), 77 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 003899b89..25212c14c 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -10,19 +10,19 @@ ### TRAJECTORY ### # arbitrary trajectory - # dict_trajectory = { - # "degrees": [2], - # "knot_vectors": [[0.0, 0.0, 0.0, 0.333, 0.666, 1.0, 1.0, 1.0]], - # "control_points": np.array( - # [ - # [0.0, 0.0, 0.0], - # [0.0, 0.0, 5.0], - # [10.0, 5.0, 0.0], - # [15.0, 0.0, -5.0], - # [20.0, 0.0, 0.0], - # ] - # ), - # } + dict_trajectory = { + "degrees": [2], + "knot_vectors": [[0.0, 0.0, 0.0, 0.333, 0.666, 1.0, 1.0, 1.0]], + "control_points": np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 5.0], + [10.0, 5.0, 0.0], + [15.0, 0.0, -5.0], + [20.0, 0.0, 0.0], + ] + ), + } # 2D questionmark # dict_trajectory = { @@ -46,78 +46,78 @@ # } # init trajectory as bspline - # closed 3D questionmark - dict_trajectory = { - "degrees": [3], - "knot_vectors": [ - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.1, - 0.2, - 0.3, - 0.4, - 0.5, - 0.6, - 0.8, - 0.9, - 1.0, - 1.0, - 1.0, - 1.0, - ] - ], - "control_points": np.array( - [ - [0.5, 0, 0], - [0.5, 2, 0.3], - [1.0, 3, 0.1], - [2.0, 4, -0.1], - [2.15, 5, -0.2], - [1.8, 5.9, -0.4], - [1.0, 6.2, -0.3], - [-0.25, 6, -0.1], - [-0.5, 5.0, 0.1], - [-2.0, 4.0, 0.2], - [-1, 3.0, 0.1], - [0.5, 0.0, 0.0], - ] - ), - } + # # closed 3D questionmark + # dict_trajectory = { + # "degrees": [3], + # "knot_vectors": [ + # [ + # 0.0, + # 0.0, + # 0.0, + # 0.0, + # 0.1, + # 0.2, + # 0.3, + # 0.4, + # 0.5, + # 0.6, + # 0.8, + # 0.9, + # 1.0, + # 1.0, + # 1.0, + # 1.0, + # ] + # ], + # "control_points": np.array( + # [ + # [0.5, 0, 0], + # [0.5, 2, 0.3], + # [1.0, 3, 0.1], + # [2.0, 4, -0.1], + # [2.15, 5, -0.2], + # [1.8, 5.9, -0.4], + # [1.0, 6.2, -0.3], + # [-0.25, 6, -0.1], + # [-0.5, 5.0, 0.1], + # [-2.0, 4.0, 0.2], + # [-1, 3.0, 0.1], + # [0.5, 0.0, 0.0], + # ] + # ), + # } trajectory = splinepy.BSpline(**dict_trajectory) # alternatively, use helpme to create a trajectory # trajectory = splinepy.helpme.create.circle(10) # insert knots and control points - trajectory.uniform_refine(0, 1) + trajectory.uniform_refine(0, 3) ### CROSS SECTION ### - # dict_cross_section = { - # "degrees": [3], - # "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], - # "control_points": np.array( - # [ - # [0.0, 0.0, 0.0], - # [1.0, 2.0, 0.0], - # [2.0, 0.0, 0.0], - # [3.0, -2.0, 0.0], - # [4.0, 0.0, 0.0], - # ] - # ), - # } + dict_cross_section = { + "degrees": [3], + "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], + "control_points": np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 2.0, 0.0], + [2.0, 0.0, 0.0], + [3.0, -2.0, 0.0], + [4.0, 0.0, 0.0], + ] + ), + } - # # init cross section as bspline - # cross_section = splinepy.BSpline(**dict_cross_section) + # init cross section as bspline + cross_section = splinepy.BSpline(**dict_cross_section) # alternatively, use helpme to create a cross section - cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs + # cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (default) - cs_nv = np.array([0, 0, 1]) + cs_nv = np.array([0, 1, 1]) ### SWEEP ### swept_surface = splinepy.helpme.create.swept( @@ -125,7 +125,7 @@ cross_section=cross_section, cross_section_normal=cs_nv, auto_refinement=True, - set_on_trajectory=True, + set_on_trajectory=False, ) ### VISUALIZATION ### diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 0eca124bf..4a46b0242 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -308,9 +308,14 @@ def swept( """ Sweeps a cross-section along a trajectory. The cross-section receives rotation into the direction of the trajectory tangent - vector and is then placed at the evaluation points of the - trajectory's knots. - Fundamental ideas can be found in the NURBS Book, Piegl & Tiller, + vector and is then placed either at the evaluation points of the + trajectory's knots or at the trajectory's control points. This + depends on the value of the set_on_trajectory parameter. + + The sweeping process has some limitations, since the cross-section + cannot be preserved exactly along the whole trajectory. + + The maths behind can be found in the NURBS Book, Piegl & Tiller, 2nd edition, chapter 10.4 Swept Surfaces. Parameters @@ -378,7 +383,7 @@ def transformation_matrices(traj, par_value): vec = [-x[1], x[0], -x[2]] B = [] # avoid dividing by zero - if _np.linalg.norm(_np.cross(x, vec)) > 1e-10: + if _np.linalg.norm(_np.cross(x, vec)) > _settings.TOLERANCE: B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) else: vec = [x[2], -x[1], x[0]] @@ -465,7 +470,7 @@ def transformation_matrices(traj, par_value): [_np.sin(angle_of_cs_normal), 0, _np.cos(angle_of_cs_normal)], ] ) - R = _np.where(_np.abs(R) < 1e-10, 0, R) + R = _np.where(_np.abs(R) < _settings.TOLERANCE, 0, R) return A, R From ecc32ad0741f2099a9dda3beb50dd4e5bf13395d Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 12:57:40 +0200 Subject: [PATCH 033/171] Fix: get rid of x,y,z --- splinepy/helpme/create.py | 114 +++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 4a46b0242..a85a24d79 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -352,12 +352,16 @@ def swept( if not isinstance(trajectory, _Spline): raise NotImplementedError("Sweep only works for splines") if not trajectory.para_dim == 1: - raise NotImplementedError("Trajectory must be 1D") + raise NotImplementedError( + "Trajectory must be of parametric dimension 1" + ) if cross_section_normal is not None and not len(cross_section_normal) == 3: - raise ValueError("Cross section normal must be 3D") + raise ValueError("Cross section normal must be a 3D vector") if not isinstance(auto_refinement, bool): raise ValueError("auto_refinement must be a boolean") + if not isinstance(set_on_trajectory, bool): + raise ValueError("set_on_trajectory must be a boolean") # setting default value for cross_section_normal if cross_section_normal is None: @@ -375,90 +379,96 @@ def transformation_matrices(traj, par_value): ### TRANSFORMATION MATRICES ### - # tangent vector x on trajectory at parameter value 0 - x = traj.derivative([par_value[0]], [1]) - x = (x / _np.linalg.norm(x)).ravel() + # tangent vector 'e1' of trajectory at parameter value 0 + e1 = traj.derivative([par_value[0]], [1]) + e1 = (e1 / _np.linalg.norm(e1)).ravel() - # evaluating a vector normal to x - vec = [-x[1], x[0], -x[2]] + # evaluating a vector normal to e1 + vec = [-e1[1], e1[0], -e1[2]] B = [] # avoid dividing by zero - if _np.linalg.norm(_np.cross(x, vec)) > _settings.TOLERANCE: - B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) + if _np.linalg.norm(_np.cross(e1, vec)) > _settings.TOLERANCE: + B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) else: - vec = [x[2], -x[1], x[0]] - B.append(_np.cross(x, vec) / _np.linalg.norm(_np.cross(x, vec))) + vec = [e1[2], -e1[1], e1[0]] + B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) - # initialize transformation matrices and x-collection + # initialize transformation matrices and e1-collection T = [] A = [] - x_collection = [] + tang_collection = [] # evaluating transformation matrices for each trajectory point for i in range(len(par_value)): # calculation according to NURBS Book, eq. 10.27 - # tangent vector x on trajectory at parameter value i - x = traj.derivative([par_value[i]], [1]) - x = (x / _np.linalg.norm(x)).ravel() - x_collection.append(x) - - # projecting B_(i) onto the plane normal to x - B.append(B[i] - _np.dot(B[i], x) * x) + # tangent vector e1 on trajectory at parameter value i + e1 = traj.derivative([par_value[i]], [1]) + e1 = (e1 / _np.linalg.norm(e1)).ravel() + # collecting tangent vectors for later use + tang_collection.append(e1) + + # projecting B_(i) onto the plane normal to e1 + B.append(B[i] - _np.dot(B[i], e1) * e1) B[i + 1] = B[i + 1] / _np.linalg.norm(B[i + 1]) - # defining y and z axis-vectors - z = B[i + 1] - y = _np.cross(z, x) + # defining e2 and e3 vectors + e3 = B[i + 1] + e2 = _np.cross(e3, e1) # array of transformation matrices from global to local coordinates - T.append(_np.vstack((x, y, z))) + T.append(_np.vstack((e1, e2, e3))) # array of transformation matrices from local to global coordinates A.append(_np.linalg.inv(T[i])) - # own procedure, if trajectory is closed and B[0] != B[-1] + # separate procedure, if trajectory is closed and B[0] != B[-1] + # recalculate B-vector and middle the values between B and B_rec # according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483 - if _np.array_equal( + is_trajectory_closed = _np.array_equal( traj.evaluate([[0]]), traj.evaluate([par_value[-1]]) - ) and not _np.array_equal(B[0], B[-1]): + ) + is_B_start_equal_B_end = _np.array_equal(B[0], B[-1]) + + if is_trajectory_closed and not is_B_start_equal_B_end: # reset transformation matrices T = [] A = [] - B_reverse = [None] * len(B) - B_reverse[0] = B[-1] + # preallocate B_rec + B_rec = [None] * len(B) + # make sure start of B_rec is equal to the end of B + B_rec[0] = B[-1] + # redo the calculation of B using tang_collection from before + # in order to avoid recalculating the tangent vectors; + # calculation according to NURBS Book, eq. 10.27 for i in range(len(par_value)): - # redo the calculation of B using x_collection from before - # according to NURBS Book, eq. 10.27 - B_reverse[i + 1] = ( - B_reverse[i] - - _np.dot(B_reverse[i], x_collection[i]) * x_collection[i] - ) - B_reverse[i + 1] = B_reverse[i + 1] / _np.linalg.norm( - B_reverse[i + 1] + B_rec[i + 1] = ( + B_rec[i] + - _np.dot(B_rec[i], tang_collection[i]) + * tang_collection[i] ) - # middle point between B and B_reverse - B_reverse[i + 1] = (B[i + 1] + B_reverse[i + 1]) * 0.5 - B_reverse[i + 1] = B_reverse[i + 1] / _np.linalg.norm( - B_reverse[i + 1] - ) - # defining y and z axis-vectors - z = B_reverse[i + 1] - y = _np.cross(z, x_collection[i]) + B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) + # middle point between B and B_rec + B_rec[i + 1] = (B[i + 1] + B_rec[i + 1]) * 0.5 + # normalizing B_rec + B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) + # defining e2 and e3 axis-vectors + e3 = B_rec[i + 1] + e2 = _np.cross(e3, tang_collection[i]) # array of transformation matrices from global to local coordinates - T.append(_np.vstack((x_collection[i], y, z))) + T.append(_np.vstack((tang_collection[i], e2, e3))) # array of transformation matrices from local to global coordinates A.append(_np.linalg.inv(T[i])) # check if the beginning and the end of the B-vector are the same - if not _np.allclose(B_reverse[0], B_reverse[-1], rtol=1e-3): + if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3): _log.warning( "Vector calculation is not exact due to the " "trajectory being closed in an uncommon way." ) - ### ROTATION MATRIX AROUND Y ### + ### ROTATION MATRIX AROUND e2 VECTOR ### angle_of_cs_normal = _np.arctan2( cross_section_normal[2], cross_section_normal[0] @@ -559,6 +569,7 @@ def transformation_matrices(traj, par_value): swept_spline_cps = [] for i, par_val in enumerate(par_value): temp_csp = [] + # evaluate trajectory if user wants to set cross section on traj. if set_on_trajectory: evals = trajectory.evaluate([par_val]).ravel() # place every control point of cross section separately @@ -567,8 +578,8 @@ def transformation_matrices(traj, par_value): normal_cscp = _np.matmul(R, cscp) # transform cross section to global coordinates normal_cscp = _np.matmul(A[i], normal_cscp) - # check whether user wants to place cross section - # at evaluation points or control points of trajectory + # check if user wants to place cross section at + # evaluation points or control points of trajectory if set_on_trajectory: # translate cross section to trajectory evaluation point normal_cscp += evals @@ -593,7 +604,7 @@ def transformation_matrices(traj, par_value): ), } - # check if spline is rational + # add weights properly if spline is rational if cross_section.is_rational or trajectory.is_rational: def weights(spline): @@ -607,7 +618,6 @@ def weights(spline): trajectory_weights, cross_section_weights ).reshape(-1, 1) spline_type = _NURBS - else: spline_type = _BSpline From 7ae92319ddc78a122bc712456f04cb643a16cc12 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 13:29:00 +0200 Subject: [PATCH 034/171] Fix: embedded calc of transf matrices in normal code - without function now --- splinepy/helpme/create.py | 218 ++++++++++++++++++-------------------- 1 file changed, 105 insertions(+), 113 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index a85a24d79..c3131bee1 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -375,115 +375,6 @@ def swept( par_value = trajectory.greville_abscissae() par_value = par_value.reshape(-1, 1) - def transformation_matrices(traj, par_value): - - ### TRANSFORMATION MATRICES ### - - # tangent vector 'e1' of trajectory at parameter value 0 - e1 = traj.derivative([par_value[0]], [1]) - e1 = (e1 / _np.linalg.norm(e1)).ravel() - - # evaluating a vector normal to e1 - vec = [-e1[1], e1[0], -e1[2]] - B = [] - # avoid dividing by zero - if _np.linalg.norm(_np.cross(e1, vec)) > _settings.TOLERANCE: - B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) - else: - vec = [e1[2], -e1[1], e1[0]] - B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) - - # initialize transformation matrices and e1-collection - T = [] - A = [] - tang_collection = [] - - # evaluating transformation matrices for each trajectory point - for i in range(len(par_value)): - # calculation according to NURBS Book, eq. 10.27 - # tangent vector e1 on trajectory at parameter value i - e1 = traj.derivative([par_value[i]], [1]) - e1 = (e1 / _np.linalg.norm(e1)).ravel() - # collecting tangent vectors for later use - tang_collection.append(e1) - - # projecting B_(i) onto the plane normal to e1 - B.append(B[i] - _np.dot(B[i], e1) * e1) - B[i + 1] = B[i + 1] / _np.linalg.norm(B[i + 1]) - - # defining e2 and e3 vectors - e3 = B[i + 1] - e2 = _np.cross(e3, e1) - - # array of transformation matrices from global to local coordinates - T.append(_np.vstack((e1, e2, e3))) - - # array of transformation matrices from local to global coordinates - A.append(_np.linalg.inv(T[i])) - - # separate procedure, if trajectory is closed and B[0] != B[-1] - # recalculate B-vector and middle the values between B and B_rec - # according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483 - is_trajectory_closed = _np.array_equal( - traj.evaluate([[0]]), traj.evaluate([par_value[-1]]) - ) - is_B_start_equal_B_end = _np.array_equal(B[0], B[-1]) - - if is_trajectory_closed and not is_B_start_equal_B_end: - # reset transformation matrices - T = [] - A = [] - # preallocate B_rec - B_rec = [None] * len(B) - # make sure start of B_rec is equal to the end of B - B_rec[0] = B[-1] - # redo the calculation of B using tang_collection from before - # in order to avoid recalculating the tangent vectors; - # calculation according to NURBS Book, eq. 10.27 - for i in range(len(par_value)): - B_rec[i + 1] = ( - B_rec[i] - - _np.dot(B_rec[i], tang_collection[i]) - * tang_collection[i] - ) - B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) - # middle point between B and B_rec - B_rec[i + 1] = (B[i + 1] + B_rec[i + 1]) * 0.5 - # normalizing B_rec - B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) - # defining e2 and e3 axis-vectors - e3 = B_rec[i + 1] - e2 = _np.cross(e3, tang_collection[i]) - - # array of transformation matrices from global to local coordinates - T.append(_np.vstack((tang_collection[i], e2, e3))) - - # array of transformation matrices from local to global coordinates - A.append(_np.linalg.inv(T[i])) - - # check if the beginning and the end of the B-vector are the same - if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3): - _log.warning( - "Vector calculation is not exact due to the " - "trajectory being closed in an uncommon way." - ) - - ### ROTATION MATRIX AROUND e2 VECTOR ### - - angle_of_cs_normal = _np.arctan2( - cross_section_normal[2], cross_section_normal[0] - ) - R = _np.array( - [ - [_np.cos(angle_of_cs_normal), 0, _np.sin(angle_of_cs_normal)], - [0, 1, 0], - [_np.sin(angle_of_cs_normal), 0, _np.cos(angle_of_cs_normal)], - ] - ) - R = _np.where(_np.abs(R) < _settings.TOLERANCE, 0, R) - - return A, R - ### REFINEMENT OF TRAJECTORY ### if auto_refinement: @@ -553,6 +444,110 @@ def transformation_matrices(traj, par_value): par_value = trajectory.greville_abscissae() par_value = par_value.reshape(-1, 1) + ### TRANSFORMATION MATRICES ### + + # tangent vector 'e1' of trajectory at parameter value 0 + e1 = trajectory.derivative([par_value[0]], [1]) + e1 = (e1 / _np.linalg.norm(e1)).ravel() + + # evaluating a vector normal to e1 + vec = [-e1[1], e1[0], -e1[2]] + B = [] + # avoid dividing by zero + if _np.linalg.norm(_np.cross(e1, vec)) > _settings.TOLERANCE: + B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) + else: + vec = [e1[2], -e1[1], e1[0]] + B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) + + # initialize transformation matrices and tangent-vector-collection + T = [] + A = [] + tang_collection = [] + + # evaluating transformation matrices for each trajectory point + for i in range(len(par_value)): + # calculation according to NURBS Book, eq. 10.27 + # tangent vector e1 on trajectory at parameter value i + e1 = trajectory.derivative([par_value[i]], [1]) + e1 = (e1 / _np.linalg.norm(e1)).ravel() + # collecting tangent vectors for later use + tang_collection.append(e1) + + # projecting B_(i) onto the plane normal to e1 + B.append(B[i] - _np.dot(B[i], e1) * e1) + B[i + 1] = B[i + 1] / _np.linalg.norm(B[i + 1]) + + # defining e2 and e3 vectors + e3 = B[i + 1] + e2 = _np.cross(e3, e1) + + # array of transformation matrices from global to local coordinates + T.append(_np.vstack((e1, e2, e3))) + + # array of transformation matrices from local to global coordinates + A.append(_np.linalg.inv(T[i])) + + # separate procedure, if trajectory is closed and B[0] != B[-1] + # recalculate B-vector and middle the values between B and B_rec + # according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483 + is_trajectory_closed = _np.array_equal( + trajectory.evaluate([[0]]), trajectory.evaluate([par_value[-1]]) + ) + is_B_start_equal_B_end = _np.array_equal(B[0], B[-1]) + + if is_trajectory_closed and not is_B_start_equal_B_end: + # reset transformation matrices + T = [] + A = [] + # preallocate B_rec + B_rec = [None] * len(B) + # make sure start of B_rec is equal to the end of B + B_rec[0] = B[-1] + # redo the calculation of B using tang_collection from before + # in order to avoid recalculating the tangent vectors; + # calculation according to NURBS Book, eq. 10.27 + for i in range(len(par_value)): + B_rec[i + 1] = ( + B_rec[i] + - _np.dot(B_rec[i], tang_collection[i]) * tang_collection[i] + ) + B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) + # middle point between B and B_rec + B_rec[i + 1] = (B[i + 1] + B_rec[i + 1]) * 0.5 + # normalizing B_rec + B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) + # defining e2 and e3 axis-vectors + e3 = B_rec[i + 1] + e2 = _np.cross(e3, tang_collection[i]) + + # array of transformation matrices from global to local coordinates + T.append(_np.vstack((tang_collection[i], e2, e3))) + + # array of transformation matrices from local to global coordinates + A.append(_np.linalg.inv(T[i])) + + # check if the beginning and the end of the B-vector are the same + if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3): + _log.warning( + "Vector calculation is not exact due to the " + "trajectory being closed in an uncommon way." + ) + + ### ROTATION MATRIX AROUND e2 VECTOR ### + + angle_of_cs_normal = _np.arctan2( + cross_section_normal[2], cross_section_normal[0] + ) + R = _np.array( + [ + [_np.cos(angle_of_cs_normal), 0, _np.sin(angle_of_cs_normal)], + [0, 1, 0], + [_np.sin(angle_of_cs_normal), 0, _np.cos(angle_of_cs_normal)], + ] + ) + R = _np.where(_np.abs(R) < _settings.TOLERANCE, 0, R) + ### SWEEPING PROCESS ### # evaluate center of cross section and translate to origin @@ -562,14 +557,11 @@ def transformation_matrices(traj, par_value): ).ravel() cross_section.control_points -= cs_center - # evaluate transformation matrices for every trajectory point - A, R = transformation_matrices(trajectory, par_value) - # set cross section control points along trajectory swept_spline_cps = [] for i, par_val in enumerate(par_value): temp_csp = [] - # evaluate trajectory if user wants to set cross section on traj. + # evaluate trajectory if user wants to set cross section on trajectory. if set_on_trajectory: evals = trajectory.evaluate([par_val]).ravel() # place every control point of cross section separately From 99ab4e407d365f3797fbcdca95fe444551ba3731 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 13:39:05 +0200 Subject: [PATCH 035/171] Add: debug log at cs_normal_vector --- examples/show_swept.py | 2 +- splinepy/helpme/create.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 25212c14c..5139c7036 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -117,7 +117,7 @@ # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (default) - cs_nv = np.array([0, 1, 1]) + cs_nv = np.array([0, 0, 1]) ### SWEEP ### swept_surface = splinepy.helpme.create.swept( diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index c3131bee1..e08b6d50b 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -366,6 +366,8 @@ def swept( # setting default value for cross_section_normal if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) + # add debug message + _log.debug("No cross section normal given. Defaulting to [0, 0, 1].") # make copies so we can work on it inplace trajectory = trajectory.create.embedded(3) From b556010471650d457fe096891a2ea2efca60c35a Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 3 Jul 2024 13:43:49 +0200 Subject: [PATCH 036/171] Add: debug log for B division by zero --- splinepy/helpme/create.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index e08b6d50b..526fa069e 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -461,6 +461,10 @@ def swept( else: vec = [e1[2], -e1[1], e1[0]] B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) + # add debug message + _log.debug( + "Division by zero occurred. Using alternative vector for B." + ) # initialize transformation matrices and tangent-vector-collection T = [] From 390302ee1e4a03a5e3256ee7b883a8124234881c Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 8 Jul 2024 10:41:55 +0200 Subject: [PATCH 037/171] Add: functionality to rotate cross_section around traj-tangent-vector --- splinepy/helpme/create.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 526fa069e..b3fd7a67d 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -304,6 +304,7 @@ def swept( cross_section_normal=None, auto_refinement=False, set_on_trajectory=False, + rotation_adaption=None, ): """ Sweeps a cross-section along a trajectory. The cross-section @@ -335,6 +336,18 @@ def swept( points of the trajectory's knots. If False, the cross-section will be placed at the control points of the trajectory. Default is False. + rotation_adaption : float + Angle in radians by which the cross-section is rotated around + the trajectory tangent vector. This is an additional rotation + if the user wants to adapt the cross-section rotation. + Example with rectangular crossection: + x + x x x x x x x + x x x x + x x --> rotation around pi/4 --> x x + x x x x + x x x x x x x + x Returns ------- @@ -540,18 +553,28 @@ def swept( "trajectory being closed in an uncommon way." ) - ### ROTATION MATRIX AROUND e2 VECTOR ### + ### ROTATION MATRIX ### + # calculate angle of cross section normal vector around e2-axis angle_of_cs_normal = _np.arctan2( cross_section_normal[2], cross_section_normal[0] ) - R = _np.array( - [ - [_np.cos(angle_of_cs_normal), 0, _np.sin(angle_of_cs_normal)], - [0, 1, 0], - [_np.sin(angle_of_cs_normal), 0, _np.cos(angle_of_cs_normal)], - ] + + # calculate rotation matrix for cross section normal vector + R = _arr.rotation_matrix_around_axis( + axis=[0, 1, 0], rotation=angle_of_cs_normal, degree=False ) + + # rotate cross section around trajectory tangent vector if wanted + if rotation_adaption is not None: + R = _np.matmul( + _arr.rotation_matrix_around_axis( + axis=[1, 0, 0], rotation=rotation_adaption, degree=False + ), + R, + ) + + # remove numerical noise R = _np.where(_np.abs(R) < _settings.TOLERANCE, 0, R) ### SWEEPING PROCESS ### From d8ea3d223e168e9382d0d1281129768b3ad8a9db Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 8 Jul 2024 10:42:46 +0200 Subject: [PATCH 038/171] Add: test function to compare swept and extruded --- tests/helpme/test_create.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index 1c79c7b23..9200a2734 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -419,3 +419,24 @@ def test_swept_rational_splines(): result = splinepy.helpme.create.swept(cross_section, trajectory) assert result is not None assert result.is_rational + + +def test_swept_to_extruded(): + cross_section = splinepy.NURBS( + degrees=[2], + control_points=[[-0.5, -1 / 3], [0, 2 / 3], [0.5, -1 / 3]], + weights=[1, 0.5, 1], + knot_vectors=[[0, 0, 0, 1, 1, 1]], + ) + trajectory = splinepy.NURBS( + degrees=[1], + control_points=[[0, 0, 0], [0, 0, 3]], + weights=[1, 1], + knot_vectors=[[0, 0, 1, 1]], + ) + + result = splinepy.helpme.create.swept( + cross_section, trajectory, rotation_adaption=np.pi / 2 + ) + extruded_result = splinepy.helpme.create.extruded(cross_section, [0, 0, 3]) + assert np.allclose(result.control_points, extruded_result.control_points) From 49d8e0679491171b23dd3bff482817695da7812a Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 8 Jul 2024 11:50:54 +0200 Subject: [PATCH 039/171] Add: test_swept_with_costum_normal now with derivative check --- tests/helpme/test_create.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index 9200a2734..bfee65a84 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -380,7 +380,11 @@ def test_swept_with_custom_normal(): result = splinepy.helpme.create.swept( cross_section, trajectory, cross_section_normal=custom_normal ) - assert result is not None + trajectory = splinepy.helpme.create.embedded(trajectory, 3) + swept_derivative = result.derivative([[0.5, 0]], [0, 1]) + traj_derivative = trajectory.derivative([[0]], [1]) + + assert np.allclose(swept_derivative, traj_derivative) def test_swept_invalid_inputs(): From 84ffe6f43f1f366ffcb42e223e83a5f524042ed1 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 10 Jul 2024 10:54:49 +0200 Subject: [PATCH 040/171] Rm: auto_refinement; Rm: clean up show_swept file --- examples/show_swept.py | 165 +++++--------------------------------- splinepy/helpme/create.py | 75 ----------------- 2 files changed, 22 insertions(+), 218 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 5139c7036..c3f047e85 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -1,5 +1,3 @@ -import sys - import gustaf as gus import numpy as np @@ -9,126 +7,54 @@ ### TRAJECTORY ### - # arbitrary trajectory + # define a questionmark-trajectory dict_trajectory = { - "degrees": [2], - "knot_vectors": [[0.0, 0.0, 0.0, 0.333, 0.666, 1.0, 1.0, 1.0]], + "degrees": [3], + "knot_vectors": [ + [0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0] + ], "control_points": np.array( [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 5.0], - [10.0, 5.0, 0.0], - [15.0, 0.0, -5.0], - [20.0, 0.0, 0.0], + [0.5, 0], # Startpunkt + [0.5, 2], + [1.0, 3], + [2.0, 4], + [2.15, 5], + [1.8, 5.9], + [1.0, 6.2], + [-0.25, 6], + [-0.5, 5], ] ), } - # 2D questionmark - # dict_trajectory = { - # "degrees": [3], - # "knot_vectors": [ - # [0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0] - # ], - # "control_points": np.array( - # [ - # [0.5, 0], # Startpunkt - # [0.5, 2], - # [1.0, 3], - # [2.0, 4], - # [2.15, 5], - # [1.8, 5.9], - # [1.0, 6.2], - # [-0.25, 6], - # [-0.5, 5], - # ] - # ), - # } - # init trajectory as bspline - - # # closed 3D questionmark - # dict_trajectory = { - # "degrees": [3], - # "knot_vectors": [ - # [ - # 0.0, - # 0.0, - # 0.0, - # 0.0, - # 0.1, - # 0.2, - # 0.3, - # 0.4, - # 0.5, - # 0.6, - # 0.8, - # 0.9, - # 1.0, - # 1.0, - # 1.0, - # 1.0, - # ] - # ], - # "control_points": np.array( - # [ - # [0.5, 0, 0], - # [0.5, 2, 0.3], - # [1.0, 3, 0.1], - # [2.0, 4, -0.1], - # [2.15, 5, -0.2], - # [1.8, 5.9, -0.4], - # [1.0, 6.2, -0.3], - # [-0.25, 6, -0.1], - # [-0.5, 5.0, 0.1], - # [-2.0, 4.0, 0.2], - # [-1, 3.0, 0.1], - # [0.5, 0.0, 0.0], - # ] - # ), - # } + # create spline of trajectory dict trajectory = splinepy.BSpline(**dict_trajectory) - # alternatively, use helpme to create a trajectory - # trajectory = splinepy.helpme.create.circle(10) - - # insert knots and control points - trajectory.uniform_refine(0, 3) + # refine trajectory by inserting knots and control points + trajectory.uniform_refine(0, 1) ### CROSS SECTION ### - dict_cross_section = { - "degrees": [3], - "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], - "control_points": np.array( - [ - [0.0, 0.0, 0.0], - [1.0, 2.0, 0.0], - [2.0, 0.0, 0.0], - [3.0, -2.0, 0.0], - [4.0, 0.0, 0.0], - ] - ), - } - - # init cross section as bspline - cross_section = splinepy.BSpline(**dict_cross_section) - # alternatively, use helpme to create a cross section - # cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs + # helpme to create a circular cross section + cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (default) cs_nv = np.array([0, 0, 1]) ### SWEEP ### + + # create swept surface swept_surface = splinepy.helpme.create.swept( trajectory=trajectory, cross_section=cross_section, cross_section_normal=cs_nv, - auto_refinement=True, set_on_trajectory=False, ) ### VISUALIZATION ### + trajectory.show_options["control_mesh"] = False cross_section.show_options["control_mesh"] = False swept_surface.show_options["control_mesh"] = False @@ -139,50 +65,3 @@ ["Swept Surface", swept_surface], resolution=101, ) - - sys.exit() - - ### EXPORT A SWEPT SPLINE ### - dict_export_cs = { - "degrees": [1], - "knot_vectors": [[0.0, 0.0, 1.0, 1.0]], - "control_points": np.array( - [ - [0.0, 0.0], - [1.0, 0.0], - ] - ), - } - export_cs = splinepy.BSpline(**dict_export_cs) - - dict_export_traj = { - "degrees": [3], - "knot_vectors": [[0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0]], - "control_points": np.array( - [ - [0.0, 0.0], - [1.0, 2.0], - [2.0, 0.0], - [3.0, -2.0], - [4.0, 0.0], - ] - ), - } - export_traj = splinepy.BSpline(**dict_export_traj) - - swept_surface = splinepy.helpme.create.swept( - trajectory=export_traj, - cross_section=export_cs, - cross_section_normal=[-1, 0, 0], - ) - - gus.show( - ["Trajectory", export_traj], - ["Cross Section", export_cs], - ["Swept Surface", swept_surface], - resolution=101, - ) - - projection = swept_surface.create.embedded(2) - gus.show(["Projection", projection], resolution=101) - splinepy.io.mfem.export("testmeshmesh.mesh", projection) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index b3fd7a67d..299ffaf98 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -302,7 +302,6 @@ def swept( cross_section, trajectory, cross_section_normal=None, - auto_refinement=False, set_on_trajectory=False, rotation_adaption=None, ): @@ -328,9 +327,6 @@ def swept( cross_section_normal : np.array Normal vector of the cross-section Default is [0, 0, 1] - auto_refinement : bool - If True, the trajectory will be refined at points of - highest curvature. Default is False. set_on_trajectory : bool If True, the cross-section will be placed at the evaluation points of the trajectory's knots. If False, the cross-section @@ -371,8 +367,6 @@ def swept( if cross_section_normal is not None and not len(cross_section_normal) == 3: raise ValueError("Cross section normal must be a 3D vector") - if not isinstance(auto_refinement, bool): - raise ValueError("auto_refinement must be a boolean") if not isinstance(set_on_trajectory, bool): raise ValueError("set_on_trajectory must be a boolean") @@ -390,75 +384,6 @@ def swept( par_value = trajectory.greville_abscissae() par_value = par_value.reshape(-1, 1) - ### REFINEMENT OF TRAJECTORY ### - - if auto_refinement: - ## inserts knots in trajectory area with highest curvature ## - - curv = [] - for i in par_value: - # calculate curvature of trajectory at parametric value i - curv.append( - round(_np.linalg.norm(trajectory.derivative([i], [2])), 2) - ) - # evaluate the par_values-vector indices of the maximum curvature points - max_curv = max(curv) - max_indices = [i for i, x in enumerate(curv) if x == max_curv] - # prepare matrix for the insertion - insertion_values = [] - # compute the new insertion values - par_values = par_value.ravel() - - for maxi in max_indices: - if maxi == 0: - insertion_values.append( - (par_values[maxi] + par_values[maxi + 1]) / 2 - ) - elif maxi == len(par_values) - 1: - insertion_values.append( - (par_values[maxi] + par_values[maxi - 1]) / 2 - ) - else: - insertion_values.append( - (par_values[maxi] + par_values[maxi - 1]) / 2 - ) - insertion_values.append( - (par_values[maxi] + par_values[maxi + 1]) / 2 - ) - - # insert knots into the trajectory's knot vector - insertion_values = _np.unique(insertion_values) - # convert knot vector to list - kv_list = trajectory.knot_vectors[0].numpy() - - # insert knots into the trajectory's knot vector - if any(value in insertion_values for value in kv_list): - add = _np.concatenate((insertion_values, kv_list)) - add = _np.unique(add) - # remove the existing knots - add = add[~_np.isin(add, kv_list)] - # check if there are any knots to insert - if len(add) == 0: - _log.warning("Auto Refinement couldn't insert knots.") - else: - trajectory.insert_knots(0, add) - # give information about the inserted knots - _log.info( - f"Auto Refinement inserted {len(add)} " - "knots into the trajectory." - ) - else: - trajectory.insert_knots(0, insertion_values) - # give information about the inserted knots - _log.info( - f"Auto Refinement inserted {len(insertion_values)} " - "knots into the trajectory." - ) - - # recalculate parameter values - par_value = trajectory.greville_abscissae() - par_value = par_value.reshape(-1, 1) - ### TRANSFORMATION MATRICES ### # tangent vector 'e1' of trajectory at parameter value 0 From 6e4f37d528f413a4de711b993e621ba9e9ab4c8a Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 12 Jul 2024 23:27:30 +0200 Subject: [PATCH 041/171] Fix: new test functions --- splinepy/helpme/create.py | 19 ++-- tests/helpme/test_create.py | 196 ++++++++++++++++++++++++++---------- 2 files changed, 155 insertions(+), 60 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 299ffaf98..7907342e7 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -355,20 +355,21 @@ def swept( from splinepy import BSpline as _BSpline from splinepy.spline import Spline as _Spline - # check input type + ### INPUT CHECKS ### + if not isinstance(cross_section, _Spline): - raise NotImplementedError("Sweep only works for splines") + raise TypeError("Sweep only works for splines") if not isinstance(trajectory, _Spline): - raise NotImplementedError("Sweep only works for splines") + raise TypeError("Sweep only works for splines") if not trajectory.para_dim == 1: - raise NotImplementedError( - "Trajectory must be of parametric dimension 1" - ) + raise TypeError("Trajectory must be of parametric dimension 1") + if not isinstance(set_on_trajectory, bool): + raise TypeError("set_on_trajectory must be a boolean") + if not isinstance(rotation_adaption, (float, int, type(None))): + raise TypeError("rotation_adaption must be a float or int") if cross_section_normal is not None and not len(cross_section_normal) == 3: raise ValueError("Cross section normal must be a 3D vector") - if not isinstance(set_on_trajectory, bool): - raise ValueError("set_on_trajectory must be a boolean") # setting default value for cross_section_normal if cross_section_normal is None: @@ -376,6 +377,8 @@ def swept( # add debug message _log.debug("No cross section normal given. Defaulting to [0, 0, 1].") + ### STARTING CALCULATIONS ### + # make copies so we can work on it inplace trajectory = trajectory.create.embedded(3) cross_section = cross_section.create.embedded(3) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index bfee65a84..c92d23483 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -1,5 +1,6 @@ import numpy as np import pytest +from gustaf.utils import arr import splinepy @@ -346,101 +347,192 @@ def test_determinant_spline( def test_swept_basic_functionality(): - cross_section = splinepy.BSpline( - degrees=[2], - control_points=[[0, 0], [0.5, 1], [1, 0]], - knot_vectors=[[0, 0, 0, 1, 1, 1]], + cross_section = splinepy.NURBS( + degrees=[2, 1], + knot_vectors=[ + [0, 0, 0, 1, 1, 1], + [0, 0, 1, 1], + ], + control_points=[ + [-1.0, 0.0], + [-1.0, 1.0], + [0.0, 1.0], + [-2.0, 0.0], + [-2.0, 2.0], + [0.0, 2.0], + ], + weights=[ + [1.0], + [2**-0.5], + [1.0], + [1.0], + [2**-0.5], + [1.0], + ], ) trajectory = splinepy.BSpline( degrees=[3], control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], ) + + # create result --> 3D body should be created result = splinepy.helpme.create.swept(cross_section, trajectory) + + # test result's type assert result is not None + assert result.is_rational + + # test invalid input + invalid_trajectory = "invalid_trajectory" + with pytest.raises(TypeError): + splinepy.helpme.create.swept(cross_section, invalid_trajectory) + + # test result's shape assert ( result.control_points.shape[0] == cross_section.control_points.shape[0] * trajectory.control_points.shape[0] ) + # test if result is 3D + assert result.dim == 3 -def test_swept_with_custom_normal(): - cross_section = splinepy.BSpline( + +def test_swept_to_extruded(): + # create linear swept surface and compare it to extruded surface + + cross_section = splinepy.NURBS( degrees=[2], - control_points=[[0, 0], [0.5, 1], [1, 0]], + control_points=[[-0.5, -1 / 3], [0, 2 / 3], [0.5, -1 / 3]], + weights=[1, 0.5, 1], knot_vectors=[[0, 0, 0, 1, 1, 1]], ) - trajectory = splinepy.BSpline( - degrees=[3], - control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], - knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], + trajectory = splinepy.NURBS( + degrees=[1], + control_points=[[0, 0, 0], [0, 0, 3]], + weights=[1, 1], + knot_vectors=[[0, 0, 1, 1]], ) - custom_normal = np.array([0, 1, 0]) + result = splinepy.helpme.create.swept( - cross_section, trajectory, cross_section_normal=custom_normal + cross_section, trajectory, rotation_adaption=np.pi / 2 ) - trajectory = splinepy.helpme.create.embedded(trajectory, 3) - swept_derivative = result.derivative([[0.5, 0]], [0, 1]) - traj_derivative = trajectory.derivative([[0]], [1]) + extruded_result = splinepy.helpme.create.extruded(cross_section, [0, 0, 3]) + assert np.allclose(result.control_points, extruded_result.control_points) - assert np.allclose(swept_derivative, traj_derivative) +def test_swept_control_point_placing(np_rng): + # check if cross-section's control points are always placed in the correct angle -def test_swept_invalid_inputs(): cross_section = splinepy.BSpline( degrees=[2], control_points=[[0, 0], [0.5, 1], [1, 0]], knot_vectors=[[0, 0, 0, 1, 1, 1]], ) - invalid_trajectory = "invalid_trajectory" - with pytest.raises(NotImplementedError): - splinepy.helpme.create.swept(cross_section, invalid_trajectory) - invalid_cross_section = "invalid_cross_section" trajectory = splinepy.BSpline( degrees=[3], - control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], + control_points=[[0, 0, 0], [1, 0, 1], [0, 0, 2], [0, 0, 3]], knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], ) - with pytest.raises(NotImplementedError): - splinepy.helpme.create.swept(invalid_cross_section, trajectory) + result = splinepy.helpme.create.swept(cross_section, trajectory) + def calculate_cs_normal_vector(cs_cp, res_cp, rand): -def test_swept_rational_splines(): - cross_section = splinepy.NURBS( + # take the control points of the chosen cross-section + taken_cs_cp = res_cp[ + rand * len(cs_cp) : rand * len(cs_cp) + len(cs_cp) + ] + P1 = taken_cs_cp[0] + P2 = taken_cs_cp[1] + P3 = taken_cs_cp[2] + + # calculate normal vector of cross-section + v1 = P2 - P1 + v2 = P3 - P1 + return np.cross(v1, v2) + + cs_cp = cross_section.control_points + res_cp = result.control_points + + # choose id of cross-section to take from result + rand = np_rng.integers(0, len(res_cp) / len(cs_cp), 1)[0] + + normal_vector = calculate_cs_normal_vector(cs_cp, res_cp, rand) + + # evaluate trajectory parameter value at the position of the cross-section + rand_traj_pos = trajectory.greville_abscissae()[rand] + # calculate tangent vector of trajectory at the position of the cross-section + rand_traj_tangent = trajectory.derivative([rand_traj_pos], 1).ravel() + + # check if normal vector is parallel to tangent vector + # --> then transformation of cross-section is correct + assert np.allclose(np.cross(rand_traj_tangent, normal_vector).all(), 0) + + ## TEST ALSO WITH CUSTOM NORMAL VECTOR ## + + # make sure that the angle between trajectory's tangent and cross-section's + # normal vector is always the same + custom_normal = np.array([1, 0, 1]) + + angle_of_custom_cs_normal = np.arctan2(custom_normal[2], custom_normal[0]) + # calculate rotation matrix for cross-section normal vector + R = arr.rotation_matrix_around_axis( + axis=[0, 1, 0], rotation=angle_of_custom_cs_normal, degree=False + ) + # in sweeping, the cross-section is rotated, we mimic this here + test_normal = np.matmul(R, custom_normal) + + start_traj_tang = trajectory.derivative([[0]], 1).ravel() + angle_at_start = np.arccos( + np.dot(start_traj_tang, test_normal) + / (np.linalg.norm(start_traj_tang) * np.linalg.norm(test_normal)) + ) + + result_with_cus_normal = splinepy.helpme.create.swept( + cross_section, trajectory, cross_section_normal=custom_normal + ) + + result_with_cus_normal_cps = result_with_cus_normal.control_points + normal_vector_custom_version = ( + calculate_cs_normal_vector(cs_cp, result_with_cus_normal_cps, rand) + * -1 + ) + + angle_at_rand = np.arccos( + np.dot(rand_traj_tangent, normal_vector_custom_version) + / ( + np.linalg.norm(rand_traj_tangent) + * np.linalg.norm(normal_vector_custom_version) + ) + ) + + assert np.allclose(angle_at_start, angle_at_rand) + + +def test_swept_cs_centering(np_rng): + # check if cross-section's center lays on the trajectory + + cross_section = splinepy.BSpline( degrees=[2], control_points=[[0, 0], [0.5, 1], [1, 0]], - weights=[1, 0.5, 1], knot_vectors=[[0, 0, 0, 1, 1, 1]], ) - trajectory = splinepy.NURBS( + trajectory = splinepy.BSpline( degrees=[3], - control_points=[[0, 0], [1, 2], [2, 3], [3, 3]], - weights=[1, 0.5, 0.5, 1], + control_points=[[0, 0, 0], [1, 2, 0], [2, 3, 0], [3, 3, 0]], knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], ) result = splinepy.helpme.create.swept(cross_section, trajectory) - assert result is not None - assert result.is_rational - -def test_swept_to_extruded(): - cross_section = splinepy.NURBS( - degrees=[2], - control_points=[[-0.5, -1 / 3], [0, 2 / 3], [0.5, -1 / 3]], - weights=[1, 0.5, 1], - knot_vectors=[[0, 0, 0, 1, 1, 1]], - ) - trajectory = splinepy.NURBS( - degrees=[1], - control_points=[[0, 0, 0], [0, 0, 3]], - weights=[1, 1], - knot_vectors=[[0, 0, 1, 1]], - ) + # choose random parameter value of trajectory + r_par_val = np_rng.random(1) + # evaluate swept spline at the chosen position of the trajectory + # --> cross-section parameter value 0.5 ensures that the center of the + # cross-section is evaluated + coords = result.evaluate([[0.5, r_par_val[0]]]).ravel() + # evaluate trajectory at the chosen position of the trajectory + ref_coords = trajectory.evaluate([r_par_val]).ravel() - result = splinepy.helpme.create.swept( - cross_section, trajectory, rotation_adaption=np.pi / 2 - ) - extruded_result = splinepy.helpme.create.extruded(cross_section, [0, 0, 3]) - assert np.allclose(result.control_points, extruded_result.control_points) + assert np.allclose(coords, ref_coords) From 31251eb8ed1d5db4e7bb3a5ca765a220de896263 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 12 Jul 2024 23:28:32 +0200 Subject: [PATCH 042/171] Rm: german comment --- examples/show_swept.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index c3f047e85..2cafeb431 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -15,7 +15,7 @@ ], "control_points": np.array( [ - [0.5, 0], # Startpunkt + [0.5, 0], [0.5, 2], [1.0, 3], [2.0, 4], From a3c8b0c06697ac66849b1ded62a2f6ed13c9f69b Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 12 Jul 2024 23:32:08 +0200 Subject: [PATCH 043/171] Add: capabilities of sweeping in the docstring --- examples/show_swept.py | 2 +- splinepy/helpme/create.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 2cafeb431..00979a049 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -15,7 +15,7 @@ ], "control_points": np.array( [ - [0.5, 0], + [0.5, 0], [0.5, 2], [1.0, 3], [2.0, 4], diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 7907342e7..dd1885b68 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -310,7 +310,9 @@ def swept( receives rotation into the direction of the trajectory tangent vector and is then placed either at the evaluation points of the trajectory's knots or at the trajectory's control points. This - depends on the value of the set_on_trajectory parameter. + depends on the value of the set_on_trajectory parameter. It can + create both a surface or a solid, depending on the dimension of + the cross-section. The sweeping process has some limitations, since the cross-section cannot be preserved exactly along the whole trajectory. From 616fc4deb4f8722ddc1705105ea5c55718d3ae39 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 12 Jul 2024 23:57:17 +0200 Subject: [PATCH 044/171] Fix: changed indentation in docstring --- splinepy/helpme/create.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index dd1885b68..e0025eeb4 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -335,17 +335,17 @@ def swept( will be placed at the control points of the trajectory. Default is False. rotation_adaption : float - Angle in radians by which the cross-section is rotated around - the trajectory tangent vector. This is an additional rotation - if the user wants to adapt the cross-section rotation. - Example with rectangular crossection: - x - x x x x x x x - x x x x - x x --> rotation around pi/4 --> x x - x x x x - x x x x x x x - x + Angle in radians by which the cross-section is rotated around + the trajectory tangent vector. This is an additional rotation + if the user wants to adapt the cross-section rotation. + Example with rectangular crossection: + x + x x x x x x x + x x x x + x x --> rotation around pi/4 --> x x + x x x x + x x x x x x x + x Returns ------- From dc0fa91e35655adc4cf44c9e1c16df37fb0dfd95 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Sat, 13 Jul 2024 00:11:23 +0200 Subject: [PATCH 045/171] Fix: added points in docstring --- splinepy/helpme/create.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index e0025eeb4..18676ba26 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -339,13 +339,13 @@ def swept( the trajectory tangent vector. This is an additional rotation if the user wants to adapt the cross-section rotation. Example with rectangular crossection: - x - x x x x x x x - x x x x - x x --> rotation around pi/4 --> x x - x x x x - x x x x x x x - x + . x + . x x x x x x x + . x x x x + . x x --> rotation around pi/4 --> x x + . x x x x + . x x x x x x x + . x Returns ------- From a52689af141f900eae2a67905019958c3aaedbea Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 9 Sep 2024 16:25:51 +0200 Subject: [PATCH 046/171] Add: show_swept-file now shows more examples --- examples/show_swept.py | 99 +++++++++++++++++++++++++++++++++------ splinepy/helpme/create.py | 2 +- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 00979a049..eb509054c 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -7,7 +7,7 @@ ### TRAJECTORY ### - # define a questionmark-trajectory + # define a hook-trajectory dict_trajectory = { "degrees": [3], "knot_vectors": [ @@ -34,34 +34,105 @@ # refine trajectory by inserting knots and control points trajectory.uniform_refine(0, 1) - ### CROSS SECTION ### + ### CROSS SECTIONS ### - # helpme to create a circular cross section - cross_section = splinepy.helpme.create.surface_circle(0.5).nurbs + # 1. create a circular line-cross-section + cross_section_circle1D = splinepy.helpme.create.circle(0.5).nurbs + + # 2. create a circular surface-cross-section + cross_section_circle2D = splinepy.helpme.create.surface_circle(0.5).nurbs + + # 3. create a rectangular surface-cross-section + cross_section_rect2D = splinepy.helpme.create.box(1, 1).nurbs # user can define the normal vector of the cross section, in case - # the cross section is not planar in the x-y plane (default) - cs_nv = np.array([0, 0, 1]) + # the cross section is not planar in the x-y plane (or the user + # wants crooked sweeping) + cs_nv = np.array([1, 0, 1]) ### SWEEP ### # create swept surface - swept_surface = splinepy.helpme.create.swept( + swept_surface_1D_circle = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_circle1D, + cross_section_normal=None, + set_on_trajectory=False, + rotation_adaption=None, + ) + + # create swept solid (circular nr. 1) + # the cross-sections are set on the trajectory's control points (default) + swept_surface_2D_circle_1 = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section, + cross_section=cross_section_circle2D, + cross_section_normal=None, + set_on_trajectory=False, + rotation_adaption=None, + ) + + # create crooked swept solid (circular nr. 2) + # the cross-sections are set on the trajectory's evaluation points + swept_surface_2D_circle_2 = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_circle2D, + cross_section_normal=None, + set_on_trajectory=True, + rotation_adaption=None, + ) + + # create swept solid set on trajectory (circular nr. 3) + # the cross-section's normal vector is not default; crooked sweeping + swept_surface_2D_circle_3 = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_circle2D, cross_section_normal=cs_nv, set_on_trajectory=False, + rotation_adaption=None, ) - ### VISUALIZATION ### + # create swept solid (rectangular nr. 1) + swept_surface_2D_rect_1 = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_rect2D, + cross_section_normal=None, + set_on_trajectory=False, + rotation_adaption=None, + ) + + # create swept solid (rectangular nr. 2) + # rotation adaption with 45 degrees + swept_surface_2D_rect_2 = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_rect2D, + cross_section_normal=None, + set_on_trajectory=False, + rotation_adaption=45 * np.pi / 180, + ) - trajectory.show_options["control_mesh"] = False - cross_section.show_options["control_mesh"] = False - swept_surface.show_options["control_mesh"] = False + ### VISUALIZATION ### gus.show( ["Trajectory", trajectory], - ["Cross Section", cross_section], - ["Swept Surface", swept_surface], + ["1D Cross Section", cross_section_circle1D], + ["Swept Surface", swept_surface_1D_circle], + resolution=101, + control_mesh=False, + ) + + gus.show( + ["2D Cross Section", cross_section_circle2D], + ["Swept Solid - Set on Control Points", swept_surface_2D_circle_1], + ["Swept Solid - Set on Evaluation Points", swept_surface_2D_circle_2], + ["Swept Solid - Crooked Sweeping", swept_surface_2D_circle_3], + resolution=101, + control_mesh=False, + ) + + gus.show( + ["New Cross Section", cross_section_rect2D], + ["Swept Solid without Rotation", swept_surface_2D_rect_1, trajectory], + ["Swept Solid with 45° Rotation", swept_surface_2D_rect_2, trajectory], resolution=101, + control_mesh=False, ) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 18676ba26..9fb9a3314 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -338,7 +338,7 @@ def swept( Angle in radians by which the cross-section is rotated around the trajectory tangent vector. This is an additional rotation if the user wants to adapt the cross-section rotation. - Example with rectangular crossection: + Example with rectangular cross-section: . x . x x x x x x x . x x x x From d5e2ad1fa4fdc6f639d706030b7c81b2a3ebfa5b Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 10 Sep 2024 18:54:40 +0200 Subject: [PATCH 047/171] Rm: .all()-function in test_create-file --- tests/helpme/test_create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index c92d23483..38f09b74a 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -468,7 +468,7 @@ def calculate_cs_normal_vector(cs_cp, res_cp, rand): # check if normal vector is parallel to tangent vector # --> then transformation of cross-section is correct - assert np.allclose(np.cross(rand_traj_tangent, normal_vector).all(), 0) + assert np.allclose(np.cross(rand_traj_tangent, normal_vector), 0) ## TEST ALSO WITH CUSTOM NORMAL VECTOR ## From a2c85663f453d19cfd9d37ca683c3dc7d2f522ce Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Wed, 11 Sep 2024 14:34:54 +0200 Subject: [PATCH 048/171] Fix: show_options and variable names in show_swept --- examples/show_swept.py | 99 +++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index eb509054c..983ac6dbb 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -36,14 +36,14 @@ ### CROSS SECTIONS ### - # 1. create a circular line-cross-section - cross_section_circle1D = splinepy.helpme.create.circle(0.5).nurbs + # 1. create a circular 1D-line-cross-section + cross_section_circle = splinepy.helpme.create.circle(0.5).nurbs - # 2. create a circular surface-cross-section - cross_section_circle2D = splinepy.helpme.create.surface_circle(0.5).nurbs + # 2. create a circular 2D-surface-cross-section + cross_section_disk = splinepy.helpme.create.surface_circle(0.5).nurbs - # 3. create a rectangular surface-cross-section - cross_section_rect2D = splinepy.helpme.create.box(1, 1).nurbs + # 3. create a rectangular 2D-surface-cross-section + cross_section_plate = splinepy.helpme.create.box(1, 1).nurbs # user can define the normal vector of the cross section, in case # the cross section is not planar in the x-y plane (or the user @@ -53,48 +53,48 @@ ### SWEEP ### # create swept surface - swept_surface_1D_circle = splinepy.helpme.create.swept( + swept_surface_circle = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section_circle1D, + cross_section=cross_section_circle, cross_section_normal=None, set_on_trajectory=False, rotation_adaption=None, ) - # create swept solid (circular nr. 1) - # the cross-sections are set on the trajectory's control points (default) - swept_surface_2D_circle_1 = splinepy.helpme.create.swept( + # create crooked swept solid (circular nr. 1) + # the cross-section's normal vector is not default; crooked sweeping + swept_solid_disk_1 = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section_circle2D, - cross_section_normal=None, + cross_section=cross_section_disk, + cross_section_normal=cs_nv, set_on_trajectory=False, rotation_adaption=None, ) - # create crooked swept solid (circular nr. 2) - # the cross-sections are set on the trajectory's evaluation points - swept_surface_2D_circle_2 = splinepy.helpme.create.swept( + # create swept solid (circular nr. 2) + # the cross-sections are set on the trajectory's control points (default) + swept_solid_disk_2 = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section_circle2D, + cross_section=cross_section_disk, cross_section_normal=None, - set_on_trajectory=True, + set_on_trajectory=False, rotation_adaption=None, ) - # create swept solid set on trajectory (circular nr. 3) - # the cross-section's normal vector is not default; crooked sweeping - swept_surface_2D_circle_3 = splinepy.helpme.create.swept( + # create swept solid (circular nr. 3) + # the cross-sections are set on the trajectory's evaluation points + swept_solid_disk_3 = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section_circle2D, - cross_section_normal=cs_nv, - set_on_trajectory=False, + cross_section=cross_section_disk, + cross_section_normal=None, + set_on_trajectory=True, rotation_adaption=None, ) # create swept solid (rectangular nr. 1) - swept_surface_2D_rect_1 = splinepy.helpme.create.swept( + swept_solid_plate_1 = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section_rect2D, + cross_section=cross_section_plate, cross_section_normal=None, set_on_trajectory=False, rotation_adaption=None, @@ -102,9 +102,9 @@ # create swept solid (rectangular nr. 2) # rotation adaption with 45 degrees - swept_surface_2D_rect_2 = splinepy.helpme.create.swept( + swept_solid_plate_2 = splinepy.helpme.create.swept( trajectory=trajectory, - cross_section=cross_section_rect2D, + cross_section=cross_section_plate, cross_section_normal=None, set_on_trajectory=False, rotation_adaption=45 * np.pi / 180, @@ -112,27 +112,46 @@ ### VISUALIZATION ### + # first window: swept surface gus.show( ["Trajectory", trajectory], - ["1D Cross Section", cross_section_circle1D], - ["Swept Surface", swept_surface_1D_circle], - resolution=101, + ["1D Cross Section", cross_section_circle], + ["Swept Surface", swept_surface_circle], + resolution=51, control_mesh=False, + control_point_ids=False, ) + # adjust show options + swept_solid_disk_2.show_options["alpha"] = 0.3 + swept_solid_disk_3.show_options["alpha"] = 0.3 + trajectory.show_options["control_points"] = False + + # second window: swept solids (circular) gus.show( - ["2D Cross Section", cross_section_circle2D], - ["Swept Solid - Set on Control Points", swept_surface_2D_circle_1], - ["Swept Solid - Set on Evaluation Points", swept_surface_2D_circle_2], - ["Swept Solid - Crooked Sweeping", swept_surface_2D_circle_3], - resolution=101, + ["2D Cross Section", cross_section_disk], + ["Swept Solid - Crooked Sweeping", swept_solid_disk_1], + [ + "Swept Solid - Set on Control Points", + swept_solid_disk_2, + trajectory, + ], + [ + "Swept Solid - Set on Evaluation Points", + swept_solid_disk_3, + trajectory, + ], + resolution=51, control_mesh=False, + control_point_ids=False, ) + # third window: swept solids (rectangular) gus.show( - ["New Cross Section", cross_section_rect2D], - ["Swept Solid without Rotation", swept_surface_2D_rect_1, trajectory], - ["Swept Solid with 45° Rotation", swept_surface_2D_rect_2, trajectory], - resolution=101, + ["New Cross Section", cross_section_plate], + ["Swept Solid without Rotation", swept_solid_plate_1], + ["Swept Solid with 45° Rotation", swept_solid_plate_2], + resolution=51, control_mesh=False, + control_point_ids=False, ) From 0b97f009fcd9d1917fad9327b0b01f0be4b17674 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 16 Sep 2024 10:55:02 +0200 Subject: [PATCH 049/171] Fix: changed comments of cs_nv --- examples/show_swept.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/show_swept.py b/examples/show_swept.py index 983ac6dbb..ecc16ecc6 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -45,9 +45,9 @@ # 3. create a rectangular 2D-surface-cross-section cross_section_plate = splinepy.helpme.create.box(1, 1).nurbs - # user can define the normal vector of the cross section, in case - # the cross section is not planar in the x-y plane (or the user - # wants crooked sweeping) + # Define a custom normal vector for the cross-section when: + # a) The cross-section is not planar in the x-y plane, or + # b) Crooked sweeping is desired cs_nv = np.array([1, 0, 1]) ### SWEEP ### From 137c10bdd78fc6bbf2c084ecbc913550551a9630 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 16 Sep 2024 11:27:58 +0200 Subject: [PATCH 050/171] Add: assertions and comments in test_basic_functionality-function in test_create-file --- tests/helpme/test_create.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index 38f09b74a..93167f1ec 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -346,6 +346,10 @@ def test_determinant_spline( ), f"{sp_i.whatami} at index {idx} failed determinant spline" +### TESTS FOR SWEEPING ### + + +# test the basic functionality of the swept-function def test_swept_basic_functionality(): cross_section = splinepy.NURBS( degrees=[2, 1], @@ -398,9 +402,30 @@ def test_swept_basic_functionality(): # test if result is 3D assert result.dim == 3 + # test if result's degrees are correct + assert np.allclose( + result.degrees, + [ + cross_section.degrees[0], + cross_section.degrees[1], + trajectory.degrees[0], + ], + ) + + # test if result's knot vectors are correct + assert np.allclose( + result.knot_vectors[0], cross_section.knot_vectors[0] + ), "Knot vector of first cross-section dimension does not match." + assert np.allclose( + result.knot_vectors[1], cross_section.knot_vectors[1] + ), "Knot vector of second cross-section dimension does not match." + assert np.allclose( + result.knot_vectors[2], trajectory.knot_vectors[0] + ), "Knot vector of trajectory does not match." + +# create linear swept surface and compare it to extruded surface def test_swept_to_extruded(): - # create linear swept surface and compare it to extruded surface cross_section = splinepy.NURBS( degrees=[2], @@ -422,8 +447,8 @@ def test_swept_to_extruded(): assert np.allclose(result.control_points, extruded_result.control_points) +# check if cross-section's control points are always placed in the correct angle def test_swept_control_point_placing(np_rng): - # check if cross-section's control points are always placed in the correct angle cross_section = splinepy.BSpline( degrees=[2], @@ -511,8 +536,8 @@ def calculate_cs_normal_vector(cs_cp, res_cp, rand): assert np.allclose(angle_at_start, angle_at_rand) +# check if cross-section's center lays on the trajectory def test_swept_cs_centering(np_rng): - # check if cross-section's center lays on the trajectory cross_section = splinepy.BSpline( degrees=[2], From 82467e1ab4530f95d2a9e95221483e9db944bee3 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 16 Sep 2024 12:07:24 +0200 Subject: [PATCH 051/171] Fix: error messages in create-file --- splinepy/helpme/create.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 9fb9a3314..7c197b176 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -371,13 +371,13 @@ def swept( raise TypeError("rotation_adaption must be a float or int") if cross_section_normal is not None and not len(cross_section_normal) == 3: - raise ValueError("Cross section normal must be a 3D vector") + raise ValueError("cross_section_normal must be a 3D vector") # setting default value for cross_section_normal if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) # add debug message - _log.debug("No cross section normal given. Defaulting to [0, 0, 1].") + _log.debug("No cross_section_normal given. Defaulting to [0, 0, 1].") ### STARTING CALCULATIONS ### From 7611ece11128ecd44156cbb56ba5c5e72e6a80b3 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Mon, 16 Sep 2024 12:49:53 +0200 Subject: [PATCH 052/171] Fix: clarified error messages at input checks of create-file --- splinepy/helpme/create.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 7c197b176..ae5eff5a6 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -355,16 +355,17 @@ def swept( from splinepy import NURBS as _NURBS from splinepy import BSpline as _BSpline - from splinepy.spline import Spline as _Spline ### INPUT CHECKS ### - if not isinstance(cross_section, _Spline): - raise TypeError("Sweep only works for splines") - if not isinstance(trajectory, _Spline): - raise TypeError("Sweep only works for splines") + if not isinstance(cross_section, (_BSpline, _NURBS)): + raise TypeError( + "cross_section must be an instance of BSpline or NURBS" + ) + if not isinstance(trajectory, (_BSpline, _NURBS)): + raise TypeError("trajectory must be an instance of BSpline or NURBS") if not trajectory.para_dim == 1: - raise TypeError("Trajectory must be of parametric dimension 1") + raise TypeError("trajectory must be of parametric dimension 1") if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") if not isinstance(rotation_adaption, (float, int, type(None))): From fa4ac39457f812192c02a6584ed2eeead28b86b6 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Tue, 17 Sep 2024 18:15:11 +0200 Subject: [PATCH 053/171] Fix: rotation matrix calculation now also for e2 entries --- splinepy/helpme/create.py | 52 +++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index ae5eff5a6..3a74ad5bf 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -373,7 +373,6 @@ def swept( if cross_section_normal is not None and not len(cross_section_normal) == 3: raise ValueError("cross_section_normal must be a 3D vector") - # setting default value for cross_section_normal if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) @@ -485,18 +484,33 @@ def swept( ) ### ROTATION MATRIX ### + # rotates cross-section normal vector into global e1 direction - # calculate angle of cross section normal vector around e2-axis - angle_of_cs_normal = _np.arctan2( - cross_section_normal[2], cross_section_normal[0] - ) + # skip calculation if cross-section normal vector is default + if _np.array_equal(cross_section_normal, _np.array([0, 0, 1])): + R = _np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]]) + # else, calculate rotation matrix for given cross-section normal vector + else: + # calculate angle of cross-section normal vector around e2-axis + angle_of_cs_normal_1 = _np.arctan2( + cross_section_normal[2], cross_section_normal[0] + ) - # calculate rotation matrix for cross section normal vector - R = _arr.rotation_matrix_around_axis( - axis=[0, 1, 0], rotation=angle_of_cs_normal, degree=False - ) + # calculate angle of cross-section normal vector around e3-axis + angle_of_cs_normal_2 = _np.arctan2( + cross_section_normal[1], cross_section_normal[2] + ) + + # calculate rotation matrix for cross-section normal vector + R1 = _arr.rotation_matrix_around_axis( + axis=[0, 1, 0], rotation=angle_of_cs_normal_1, degree=False + ) + R2 = _arr.rotation_matrix_around_axis( + axis=[0, 0, 1], rotation=angle_of_cs_normal_2, degree=False + ) + R = _np.matmul(R2, R1) - # rotate cross section around trajectory tangent vector if wanted + # rotate cross-section around trajectory tangent vector (e1) if wanted if rotation_adaption is not None: R = _np.matmul( _arr.rotation_matrix_around_axis( @@ -510,33 +524,33 @@ def swept( ### SWEEPING PROCESS ### - # evaluate center of cross section and translate to origin + # evaluate center of cross-section and translate to origin cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) cs_center = cross_section.evaluate( cross_para_center.reshape(-1, cross_section.para_dim) ).ravel() cross_section.control_points -= cs_center - # set cross section control points along trajectory + # set cross-section control points along trajectory swept_spline_cps = [] for i, par_val in enumerate(par_value): temp_csp = [] - # evaluate trajectory if user wants to set cross section on trajectory. + # evaluate trajectory if user wants to set cross-section on trajectory. if set_on_trajectory: evals = trajectory.evaluate([par_val]).ravel() - # place every control point of cross section separately + # place every control point of cross-section separately for cscp in cross_section.control_points: - # rotate cross section in trajectory direction + # rotate cross-section in trajectory direction normal_cscp = _np.matmul(R, cscp) - # transform cross section to global coordinates + # transform cross-section to global coordinates normal_cscp = _np.matmul(A[i], normal_cscp) - # check if user wants to place cross section at + # check if user wants to place cross-section at # evaluation points or control points of trajectory if set_on_trajectory: - # translate cross section to trajectory evaluation point + # translate cross-section to trajectory evaluation point normal_cscp += evals else: - # translate cross section to trajectory control point + # translate cross-section to trajectory control point normal_cscp += trajectory.control_points[i] # append control point to list temp_csp.append(normal_cscp) From 99b7d21715f09ab15d9a1ea2438f3ab4aa6bf640 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 16:54:54 +0100 Subject: [PATCH 054/171] Add: catch division by zero in trajectory-tangent calculation --- splinepy/helpme/create.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 3a74ad5bf..cfb74d4a6 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -419,6 +419,13 @@ def swept( # calculation according to NURBS Book, eq. 10.27 # tangent vector e1 on trajectory at parameter value i e1 = trajectory.derivative([par_value[i]], [1]) + if _np.linalg.norm(e1) < _settings.TOLERANCE: + e1 = tang_collection[-1] + # add debug message + _log.debug( + "Division by zero occurred. Applying an approximation " + "by using the previous tangent e1 for parametric value {par_value[i]}" + ) e1 = (e1 / _np.linalg.norm(e1)).ravel() # collecting tangent vectors for later use tang_collection.append(e1) From c736c9adcb18aebff0fa71793adb67ead52f288f Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 17:35:30 +0100 Subject: [PATCH 055/171] Fix: input checks now with spline-instance; Add: casting of rotation_adaption --- splinepy/helpme/create.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index cfb74d4a6..d0e01b73a 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -355,21 +355,27 @@ def swept( from splinepy import NURBS as _NURBS from splinepy import BSpline as _BSpline + from splinepy import Spline ### INPUT CHECKS ### - if not isinstance(cross_section, (_BSpline, _NURBS)): + if isinstance(cross_section and trajectory, Spline): + if not isinstance(cross_section, (_BSpline, _NURBS)): + raise TypeError( + "cross_section must be an instance of BSpline or NURBS" + ) + if not isinstance(trajectory, (_BSpline, _NURBS)): + raise TypeError( + "trajectory must be an instance of BSpline or NURBS" + ) + else: raise TypeError( - "cross_section must be an instance of BSpline or NURBS" + "cross_section and trajectory must be instances of Spline" ) - if not isinstance(trajectory, (_BSpline, _NURBS)): - raise TypeError("trajectory must be an instance of BSpline or NURBS") if not trajectory.para_dim == 1: raise TypeError("trajectory must be of parametric dimension 1") if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") - if not isinstance(rotation_adaption, (float, int, type(None))): - raise TypeError("rotation_adaption must be a float or int") if cross_section_normal is not None and not len(cross_section_normal) == 3: raise ValueError("cross_section_normal must be a 3D vector") @@ -519,6 +525,13 @@ def swept( # rotate cross-section around trajectory tangent vector (e1) if wanted if rotation_adaption is not None: + try: + rotation_adaption = float(rotation_adaption) + except TypeError: + raise TypeError( + "rotation_adaption must be a number (float, int) or None" + ) + R = _np.matmul( _arr.rotation_matrix_around_axis( axis=[1, 0, 0], rotation=rotation_adaption, degree=False From 6b6bc3b58469da5fa05bcde23cb94c3f2d9ab868 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 17:49:01 +0100 Subject: [PATCH 056/171] Add: temporal variable in order to avoid multiple cross-product-calculations --- splinepy/helpme/create.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index d0e01b73a..8e8f2e8b9 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -405,11 +405,13 @@ def swept( vec = [-e1[1], e1[0], -e1[2]] B = [] # avoid dividing by zero - if _np.linalg.norm(_np.cross(e1, vec)) > _settings.TOLERANCE: - B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) + temp_cross = _np.cross(e1, vec) + if _np.linalg.norm(temp_cross) > _settings.TOLERANCE: + B.append(temp_cross / _np.linalg.norm(temp_cross)) else: vec = [e1[2], -e1[1], e1[0]] - B.append(_np.cross(e1, vec) / _np.linalg.norm(_np.cross(e1, vec))) + temp_cross = _np.cross(e1, vec) + B.append(temp_cross / _np.linalg.norm(temp_cross)) # add debug message _log.debug( "Division by zero occurred. Using alternative vector for B." From 28edb1441f174ec0332cd17ce60fd562226a11c2 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 17:54:01 +0100 Subject: [PATCH 057/171] Fix: changed vector calculations to inplace syntax (where possible) --- splinepy/helpme/create.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 8e8f2e8b9..cf9b276d0 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -440,7 +440,7 @@ def swept( # projecting B_(i) onto the plane normal to e1 B.append(B[i] - _np.dot(B[i], e1) * e1) - B[i + 1] = B[i + 1] / _np.linalg.norm(B[i + 1]) + B[i + 1] /= _np.linalg.norm(B[i + 1]) # defining e2 and e3 vectors e3 = B[i + 1] @@ -476,11 +476,11 @@ def swept( B_rec[i] - _np.dot(B_rec[i], tang_collection[i]) * tang_collection[i] ) - B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) + B_rec[i + 1] /= _np.linalg.norm(B_rec[i + 1]) # middle point between B and B_rec B_rec[i + 1] = (B[i + 1] + B_rec[i + 1]) * 0.5 # normalizing B_rec - B_rec[i + 1] = B_rec[i + 1] / _np.linalg.norm(B_rec[i + 1]) + B_rec[i + 1] /= _np.linalg.norm(B_rec[i + 1]) # defining e2 and e3 axis-vectors e3 = B_rec[i + 1] e2 = _np.cross(e3, tang_collection[i]) From 153d242d27e0d12194327a105ed6a89eed8713a4 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 17:57:07 +0100 Subject: [PATCH 058/171] Fix: changed name of centering test-function --- tests/helpme/test_create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index 93167f1ec..04c42b476 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -537,7 +537,7 @@ def calculate_cs_normal_vector(cs_cp, res_cp, rand): # check if cross-section's center lays on the trajectory -def test_swept_cs_centering(np_rng): +def test_swept_cross_section_centering(np_rng): cross_section = splinepy.BSpline( degrees=[2], From 9ff3981b4da9b683c96b9b81caaa1774c21f2484 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 18:25:26 +0100 Subject: [PATCH 059/171] Fix: split first instance check and insert f for string --- splinepy/helpme/create.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index cf9b276d0..0feb36bec 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -359,7 +359,7 @@ def swept( ### INPUT CHECKS ### - if isinstance(cross_section and trajectory, Spline): + if isinstance(cross_section, Spline) and isinstance(trajectory, Spline): if not isinstance(cross_section, (_BSpline, _NURBS)): raise TypeError( "cross_section must be an instance of BSpline or NURBS" @@ -431,8 +431,8 @@ def swept( e1 = tang_collection[-1] # add debug message _log.debug( - "Division by zero occurred. Applying an approximation " - "by using the previous tangent e1 for parametric value {par_value[i]}" + f"Division by zero occurred. Applying an approximation " + f"by using the previous tangent e1 for parametric value {par_value[i]}" ) e1 = (e1 / _np.linalg.norm(e1)).ravel() # collecting tangent vectors for later use From 191e6752db00210801f5c96ef9f5ae5323441add Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 18:36:47 +0100 Subject: [PATCH 060/171] Fix: changed error messages semantically --- splinepy/helpme/create.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 0feb36bec..06ded541e 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -373,12 +373,14 @@ def swept( "cross_section and trajectory must be instances of Spline" ) if not trajectory.para_dim == 1: - raise TypeError("trajectory must be of parametric dimension 1") + raise TypeError("trajectory must have a parametric dimension of 1") if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") if cross_section_normal is not None and not len(cross_section_normal) == 3: - raise ValueError("cross_section_normal must be a 3D vector") + raise ValueError( + "cross_section_normal must be a 3D vector (array of length 3)" + ) # setting default value for cross_section_normal if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) @@ -494,8 +496,8 @@ def swept( # check if the beginning and the end of the B-vector are the same if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3): _log.warning( - "Vector calculation is not exact due to the " - "trajectory being closed in an uncommon way." + "Vector calculation is not exact due to the trajectory being closed" + " with non-matching tangent vectors at start and end points." ) ### ROTATION MATRIX ### From f7a51af40cb73620f711cba63278246de1fd9003 Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 18:53:41 +0100 Subject: [PATCH 061/171] Fix: changed TypeError to ValueError --- splinepy/helpme/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 06ded541e..d5f531af4 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -373,7 +373,7 @@ def swept( "cross_section and trajectory must be instances of Spline" ) if not trajectory.para_dim == 1: - raise TypeError("trajectory must have a parametric dimension of 1") + raise ValueError("trajectory must have a parametric dimension of 1") if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") From 6c6616c1968aa71cac70a2344618635f220571dd Mon Sep 17 00:00:00 2001 From: Guenther Obermair Date: Fri, 8 Nov 2024 19:01:16 +0100 Subject: [PATCH 062/171] Add: catch division by zero --- splinepy/helpme/create.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index d5f531af4..40c9903a7 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -442,6 +442,12 @@ def swept( # projecting B_(i) onto the plane normal to e1 B.append(B[i] - _np.dot(B[i], e1) * e1) + if _np.linalg.norm(B[i + 1]) < _settings.TOLERANCE: + _log.warning( + f"Vector B[{i + 1}] is close to zero. Adjusting " + f"to avoid division by zero." + ) + B[i + 1] += _settings.TOLERANCE B[i + 1] /= _np.linalg.norm(B[i + 1]) # defining e2 and e3 vectors @@ -478,6 +484,12 @@ def swept( B_rec[i] - _np.dot(B_rec[i], tang_collection[i]) * tang_collection[i] ) + if _np.linalg.norm(B_rec[i + 1]) < _settings.TOLERANCE: + _log.warning( + f"Vector B_rec[{i + 1}] is close to zero. Adjusting " + f"to avoid division by zero." + ) + B_rec[i + 1] += _settings.TOLERANCE B_rec[i + 1] /= _np.linalg.norm(B_rec[i + 1]) # middle point between B and B_rec B_rec[i + 1] = (B[i + 1] + B_rec[i + 1]) * 0.5 From f740098791a05382906a5bd0e14399adaefea2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Tue, 14 Apr 2026 16:29:26 +0200 Subject: [PATCH 063/171] Add: anchor functionality; Fix: vectorize for-loop; Fix: use np.allclose; Fix: catch first trajectory tangent error --- splinepy/helpme/create.py | 128 ++++++++++++++++++++++++++++---------- 1 file changed, 94 insertions(+), 34 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 40c9903a7..726d5a5de 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -302,6 +302,7 @@ def swept( cross_section, trajectory, cross_section_normal=None, + anchor="auto", set_on_trajectory=False, rotation_adaption=None, ): @@ -329,6 +330,13 @@ def swept( cross_section_normal : np.array Normal vector of the cross-section Default is [0, 0, 1] + anchor : str + Strategy used to determine which point of the cross-section is + placed on the trajectory. Supported values are: + ``"auto"``, ``"parametric"``, ``"control_box"``, + and ``"geometry_box"``. + ``"auto"`` uses ``"geometry_box"`` for curve cross-sections and + ``"parametric"`` for surface cross-sections. set_on_trajectory : bool If True, the cross-section will be placed at the evaluation points of the trajectory's knots. If False, the cross-section @@ -376,6 +384,8 @@ def swept( raise ValueError("trajectory must have a parametric dimension of 1") if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") + if not isinstance(anchor, str): + raise TypeError("anchor must be a string") if cross_section_normal is not None and not len(cross_section_normal) == 3: raise ValueError( @@ -387,6 +397,17 @@ def swept( # add debug message _log.debug("No cross_section_normal given. Defaulting to [0, 0, 1].") + anchor = anchor.lower() + if anchor == "auto": + anchor = ( + "geometry_box" if cross_section.para_dim == 1 else "parametric" + ) + if anchor not in {"parametric", "control_box", "geometry_box"}: + raise ValueError( + "anchor must be one of 'auto', 'parametric', " + "'control_box', or 'geometry_box'" + ) + ### STARTING CALCULATIONS ### # make copies so we can work on it inplace @@ -401,7 +422,13 @@ def swept( # tangent vector 'e1' of trajectory at parameter value 0 e1 = trajectory.derivative([par_value[0]], [1]) - e1 = (e1 / _np.linalg.norm(e1)).ravel() + e1_norm = _np.linalg.norm(e1) + if e1_norm < _settings.TOLERANCE: + raise ValueError( + "Cannot determine initial sweep direction from trajectory " + "because the first tangent is too small." + ) + e1 = (e1 / e1_norm).ravel() # evaluating a vector normal to e1 vec = [-e1[1], e1[0], -e1[2]] @@ -429,14 +456,22 @@ def swept( # calculation according to NURBS Book, eq. 10.27 # tangent vector e1 on trajectory at parameter value i e1 = trajectory.derivative([par_value[i]], [1]) - if _np.linalg.norm(e1) < _settings.TOLERANCE: - e1 = tang_collection[-1] + e1_norm = _np.linalg.norm(e1) + if e1_norm < _settings.TOLERANCE: + if tang_collection: + e1 = tang_collection[-1] + e1_norm = _np.linalg.norm(e1) + else: + raise ValueError( + "Cannot determine sweep direction because the first " + "trajectory tangent is too small." + ) # add debug message _log.debug( f"Division by zero occurred. Applying an approximation " f"by using the previous tangent e1 for parametric value {par_value[i]}" ) - e1 = (e1 / _np.linalg.norm(e1)).ravel() + e1 = (e1 / e1_norm).ravel() # collecting tangent vectors for later use tang_collection.append(e1) @@ -458,15 +493,20 @@ def swept( T.append(_np.vstack((e1, e2, e3))) # array of transformation matrices from local to global coordinates - A.append(_np.linalg.inv(T[i])) + A.append(T[i].T) # separate procedure, if trajectory is closed and B[0] != B[-1] # recalculate B-vector and middle the values between B and B_rec # according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483 - is_trajectory_closed = _np.array_equal( - trajectory.evaluate([[0]]), trajectory.evaluate([par_value[-1]]) + is_trajectory_closed = _np.allclose( + trajectory.evaluate([[0]]), + trajectory.evaluate([par_value[-1]]), + atol=_settings.TOLERANCE, + rtol=1e-8, + ) + is_B_start_equal_B_end = _np.allclose( + B[0], B[-1], atol=_settings.TOLERANCE, rtol=1e-8 ) - is_B_start_equal_B_end = _np.array_equal(B[0], B[-1]) if is_trajectory_closed and not is_B_start_equal_B_end: # reset transformation matrices @@ -503,7 +543,7 @@ def swept( T.append(_np.vstack((tang_collection[i], e2, e3))) # array of transformation matrices from local to global coordinates - A.append(_np.linalg.inv(T[i])) + A.append(T[i].T) # check if the beginning and the end of the B-vector are the same if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3): @@ -561,38 +601,58 @@ def swept( ### SWEEPING PROCESS ### # evaluate center of cross-section and translate to origin - cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) - cs_center = cross_section.evaluate( - cross_para_center.reshape(-1, cross_section.para_dim) - ).ravel() - cross_section.control_points -= cs_center + if anchor == "parametric": + cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0) + cs_center = cross_section.evaluate( + cross_para_center.reshape(-1, cross_section.para_dim) + ).ravel() + elif anchor == "control_box": + cs_min = cross_section.control_points.min(axis=0) + cs_max = cross_section.control_points.max(axis=0) + cs_center = 0.5 * (cs_min + cs_max) + else: + if cross_section.para_dim == 1: + sample_resolution = max( + 101, 4 * cross_section.control_points.shape[0] + ) + else: + sample_resolution = int( + _np.ceil(625 ** (1 / cross_section.para_dim)) + ) + sample_resolution = max(5, min(25, sample_resolution)) + + sample_axes = [ + _np.linspace( + cross_section.parametric_bounds[0, i], + cross_section.parametric_bounds[1, i], + sample_resolution, + ) + for i in range(cross_section.para_dim) + ] + sample_queries = _np.stack( + _np.meshgrid(*sample_axes, indexing="ij"), + axis=-1, + ).reshape(-1, cross_section.para_dim) + sampled_points = cross_section.evaluate(sample_queries) + cs_min = sampled_points.min(axis=0) + cs_max = sampled_points.max(axis=0) + cs_center = 0.5 * (cs_min + cs_max) + + centered_cross_section_cps = cross_section.control_points - cs_center # set cross-section control points along trajectory swept_spline_cps = [] + rotated_cross_section_cps = centered_cross_section_cps @ R.T for i, par_val in enumerate(par_value): - temp_csp = [] # evaluate trajectory if user wants to set cross-section on trajectory. if set_on_trajectory: - evals = trajectory.evaluate([par_val]).ravel() - # place every control point of cross-section separately - for cscp in cross_section.control_points: - # rotate cross-section in trajectory direction - normal_cscp = _np.matmul(R, cscp) - # transform cross-section to global coordinates - normal_cscp = _np.matmul(A[i], normal_cscp) - # check if user wants to place cross-section at - # evaluation points or control points of trajectory - if set_on_trajectory: - # translate cross-section to trajectory evaluation point - normal_cscp += evals - else: - # translate cross-section to trajectory control point - normal_cscp += trajectory.control_points[i] - # append control point to list - temp_csp.append(normal_cscp) + section_origin = trajectory.evaluate([par_val]).ravel() + else: + section_origin = trajectory.control_points[i] - # collect all control points - swept_spline_cps.append(_np.array(temp_csp)) + swept_spline_cps.append( + rotated_cross_section_cps @ A[i].T + section_origin + ) # create spline dictionary dict_swept_spline = { From 187c0022c6a4d5d9757595eee2479599fb02b459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Tue, 14 Apr 2026 16:31:13 +0200 Subject: [PATCH 064/171] Fix: function signature in test-file; Add: anchor test --- tests/helpme/test_create.py | 100 +++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index 04c42b476..b31ae441c 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -381,7 +381,11 @@ def test_swept_basic_functionality(): ) # create result --> 3D body should be created - result = splinepy.helpme.create.swept(cross_section, trajectory) + result = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="parametric", + ) # test result's type assert result is not None @@ -390,7 +394,11 @@ def test_swept_basic_functionality(): # test invalid input invalid_trajectory = "invalid_trajectory" with pytest.raises(TypeError): - splinepy.helpme.create.swept(cross_section, invalid_trajectory) + splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=invalid_trajectory, + anchor="auto", + ) # test result's shape assert ( @@ -441,7 +449,10 @@ def test_swept_to_extruded(): ) result = splinepy.helpme.create.swept( - cross_section, trajectory, rotation_adaption=np.pi / 2 + cross_section=cross_section, + trajectory=trajectory, + anchor="parametric", + rotation_adaption=np.pi / 2, ) extruded_result = splinepy.helpme.create.extruded(cross_section, [0, 0, 3]) assert np.allclose(result.control_points, extruded_result.control_points) @@ -461,7 +472,11 @@ def test_swept_control_point_placing(np_rng): control_points=[[0, 0, 0], [1, 0, 1], [0, 0, 2], [0, 0, 3]], knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], ) - result = splinepy.helpme.create.swept(cross_section, trajectory) + result = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="parametric", + ) def calculate_cs_normal_vector(cs_cp, res_cp, rand): @@ -516,7 +531,10 @@ def calculate_cs_normal_vector(cs_cp, res_cp, rand): ) result_with_cus_normal = splinepy.helpme.create.swept( - cross_section, trajectory, cross_section_normal=custom_normal + cross_section=cross_section, + trajectory=trajectory, + cross_section_normal=custom_normal, + anchor="auto", ) result_with_cus_normal_cps = result_with_cus_normal.control_points @@ -549,7 +567,11 @@ def test_swept_cross_section_centering(np_rng): control_points=[[0, 0, 0], [1, 2, 0], [2, 3, 0], [3, 3, 0]], knot_vectors=[[0, 0, 0, 0, 1, 1, 1, 1]], ) - result = splinepy.helpme.create.swept(cross_section, trajectory) + result = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="parametric", + ) # choose random parameter value of trajectory r_par_val = np_rng.random(1) @@ -561,3 +583,69 @@ def test_swept_cross_section_centering(np_rng): ref_coords = trajectory.evaluate([r_par_val]).ravel() assert np.allclose(coords, ref_coords) + + +def test_swept_anchor_modes(): + cross_section = splinepy.helpme.create.circle(0.55).nurbs + cross_section.control_points[:, 0] *= 1.2 + cross_section.control_points[:, 1] *= 0.8 + cross_section.control_points[1] += np.array([0.9, -0.15]) + cross_section.control_points[2] += np.array([0.45, 0.55]) + cross_section.control_points[5] += np.array([-0.35, -0.45]) + cross_section.weights[1] *= 0.25 + cross_section.weights[2] *= 0.45 + cross_section.weights[5] *= 0.5 + + trajectory = splinepy.BSpline( + degrees=[1], + control_points=[[0, 0, 0], [0, 0, 2]], + knot_vectors=[[0, 0, 1, 1]], + ) + + swept_parametric = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="parametric", + ) + swept_control_box = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="control_box", + ) + swept_geometry_box = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="geometry_box", + ) + swept_auto = splinepy.helpme.create.swept( + cross_section=cross_section, + trajectory=trajectory, + anchor="auto", + ) + + assert np.allclose( + swept_auto.control_points, swept_geometry_box.control_points + ) + assert not np.allclose( + swept_parametric.control_points, swept_geometry_box.control_points + ) + assert not np.allclose( + swept_control_box.control_points, swept_geometry_box.control_points + ) + + n_section_cps = cross_section.control_points.shape[0] + diff_control_vs_geometry = ( + swept_control_box.control_points[:n_section_cps] + - swept_geometry_box.control_points[:n_section_cps] + ) + diff_parametric_vs_geometry = ( + swept_parametric.control_points[:n_section_cps] + - swept_geometry_box.control_points[:n_section_cps] + ) + + assert np.allclose(diff_control_vs_geometry, diff_control_vs_geometry[0]) + assert np.allclose( + diff_parametric_vs_geometry, diff_parametric_vs_geometry[0] + ) + assert not np.allclose(diff_control_vs_geometry[0], 0.0) + assert not np.allclose(diff_parametric_vs_geometry[0], 0.0) From 2c73c8e9d043b616d5abf17a3e755330b46fb791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Tue, 14 Apr 2026 16:33:09 +0200 Subject: [PATCH 065/171] Fix: function signature in example file; Add: anchor example --- examples/show_swept.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/examples/show_swept.py b/examples/show_swept.py index ecc16ecc6..54eceed16 100644 --- a/examples/show_swept.py +++ b/examples/show_swept.py @@ -45,6 +45,19 @@ # 3. create a rectangular 2D-surface-cross-section cross_section_plate = splinepy.helpme.create.box(1, 1).nurbs + # 4. create a more complex 1D-line-cross-section + # use an asymmetric rational curve so control-box and geometry-box anchors + # visibly differ + cross_section_rectangle_line = splinepy.helpme.create.circle(0.55).nurbs + cross_section_rectangle_line.control_points[:, 0] *= 1.2 + cross_section_rectangle_line.control_points[:, 1] *= 0.8 + cross_section_rectangle_line.control_points[1] += np.array([0.9, -0.15]) + cross_section_rectangle_line.control_points[2] += np.array([0.45, 0.55]) + cross_section_rectangle_line.control_points[5] += np.array([-0.35, -0.45]) + cross_section_rectangle_line.weights[1] *= 0.25 + cross_section_rectangle_line.weights[2] *= 0.45 + cross_section_rectangle_line.weights[5] *= 0.5 + # Define a custom normal vector for the cross-section when: # a) The cross-section is not planar in the x-y plane, or # b) Crooked sweeping is desired @@ -57,6 +70,7 @@ trajectory=trajectory, cross_section=cross_section_circle, cross_section_normal=None, + anchor="auto", set_on_trajectory=False, rotation_adaption=None, ) @@ -67,6 +81,7 @@ trajectory=trajectory, cross_section=cross_section_disk, cross_section_normal=cs_nv, + anchor="auto", set_on_trajectory=False, rotation_adaption=None, ) @@ -77,6 +92,7 @@ trajectory=trajectory, cross_section=cross_section_disk, cross_section_normal=None, + anchor="auto", set_on_trajectory=False, rotation_adaption=None, ) @@ -87,6 +103,7 @@ trajectory=trajectory, cross_section=cross_section_disk, cross_section_normal=None, + anchor="auto", set_on_trajectory=True, rotation_adaption=None, ) @@ -96,6 +113,7 @@ trajectory=trajectory, cross_section=cross_section_plate, cross_section_normal=None, + anchor="auto", set_on_trajectory=False, rotation_adaption=None, ) @@ -106,10 +124,48 @@ trajectory=trajectory, cross_section=cross_section_plate, cross_section_normal=None, + anchor="auto", set_on_trajectory=False, rotation_adaption=45 * np.pi / 180, ) + # create swept surfaces to compare anchor placement strategies + swept_surface_rectangle_parametric = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_rectangle_line, + cross_section_normal=None, + anchor="parametric", + set_on_trajectory=False, + rotation_adaption=None, + ) + + swept_surface_rectangle_control_box = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_rectangle_line, + cross_section_normal=None, + anchor="control_box", + set_on_trajectory=False, + rotation_adaption=None, + ) + + swept_surface_rectangle_geometry_box = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_rectangle_line, + cross_section_normal=None, + anchor="geometry_box", + set_on_trajectory=False, + rotation_adaption=None, + ) + + swept_surface_rectangle_auto = splinepy.helpme.create.swept( + trajectory=trajectory, + cross_section=cross_section_rectangle_line, + cross_section_normal=None, + anchor="auto", + set_on_trajectory=False, + rotation_adaption=None, + ) + ### VISUALIZATION ### # first window: swept surface @@ -155,3 +211,31 @@ control_mesh=False, control_point_ids=False, ) + + # fourth window: anchor placement for line cross-sections + gus.show( + ["Rectangle Line Cross Section", cross_section_rectangle_line], + [ + "Anchor: parametric", + swept_surface_rectangle_parametric, + trajectory, + ], + [ + "Anchor: control_box", + swept_surface_rectangle_control_box, + trajectory, + ], + [ + "Anchor: geometry_box", + swept_surface_rectangle_geometry_box, + trajectory, + ], + [ + "Anchor: auto", + swept_surface_rectangle_auto, + trajectory, + ], + resolution=51, + control_mesh=False, + control_point_ids=False, + ) From 01357ac976fdb3c487ba9d518bf0fba04e7bdd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Tue, 14 Apr 2026 16:34:45 +0200 Subject: [PATCH 066/171] Add: show-swept entry in README with picture --- README.md | 8 ++++++++ docs/source/_static/show_swept.png | Bin 0 -> 52706 bytes 2 files changed, 8 insertions(+) create mode 100644 docs/source/_static/show_swept.png diff --git a/README.md b/README.md index 4cf35bfcb..7d27b120b 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,14 @@ revolved = nurbs.create.revolved( splinepy.show(["extruded", extruded], ["revolved", revolved]) ``` ![create_derived](docs/source/_static/readme_create_derived.png) +You can also create swept surfaces and solids along spline trajectories. +```python +swept = splinepy.helpme.create.swept( + cross_section=splinepy.helpme.create.circle(0.5).nurbs, + trajectory=trajectory, +) +``` +![show_swept](docs/source/_static/show_swept.png) ### 4.2 Extract Using [splinepy.helpme.extract](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.extract.html#module-splinepy.helpme.extract) module, you can extract meshes (as a [gustaf](https://tataratat.github.io/gustaf/index.html) object) diff --git a/docs/source/_static/show_swept.png b/docs/source/_static/show_swept.png new file mode 100644 index 0000000000000000000000000000000000000000..37c1db052f8e3ca38c5cdf40f95642f1eded5a79 GIT binary patch literal 52706 zcmYJbWk6Kz_cc5;0z*hihqNF{he`@acS@(y-J!I!q;z+uG}7H64bqKt^Iqfq`#a8XvAm`2n6~;LR1j~K}v@};8sxK!QU)QX|F&al#maiLdq^W z`-`s0cvH>q9=4Kq4>j}zjmZR&$MS!QuQP?nV!a$A$$Fx*_WfDb2aeJ)wZpyd3EK|O zHKaLi8g-+KY&-H5$`X<6d;>~MMFruJacrPOAAR(n>8S_&I4rgI@=h+?-EE#VbzLfx z&1=|IK4(*(I3L_feonxm$>DY9f{mk~R_XHB0P;ov7spX3gphUf`bSVc`u8qkY@Git zM@I~vp#QG?dBZj#(f_VRas*83|E}GOXEy)4TcNJU$M=7CH@rnA{ol=#2S`mh8dWCu zS3Lr|`=Tp?ayCPOLne%J{iZ7K;(qV1kh?Dlmxasb z4^v_il9@7H)|{rD+@y;ePFw}nn8KB3JT@z@-w6kmXw_koa1FMOR~EzfFf9)LU>CSv zj~00N8_`947zWz3;kU#wL3!+PDv zn;9)l&2MCe5%^b}4uanIIAc_{62t87pLJWvDF(TJ>izm^Kg_$85lDWsofUzMNvf`* zk`u#Ji;JTmSx9PlclEcd>EzqxapUs+Avi3B+ja)7+?&@k;kWC70(YzB`W-DdQ)L9^ z5~8A*^R?FX8*#k$o9hd1dnI)`p663b9ir5K+tFF?_DuzDmo8ffBYN(WzG=34-4nK6 zbpiJID`a^T?drgWshZm z@BVB87Lfc$O;x~gZ8!~>Ig@J<0$!5};`ggq-l)jFOE;C3m2>~Do12>qkw5~TcCQ}T zD@zl;8QIrGIfa&LH#*&39Msg*fM?58)QXDM+eRu-XO_yEzjfT5gjon?)1$`+>(iMk zMN;?uipynhZ&Op#9m1(9(-EB(kLy@9ogF#uJD5jRRpacH)T&%hH_7;&LMa569&;9~ zf0yg$cqfR8&br;XZtArN&DX-k#K#i?N5;%7Dccn5wmyXOAO74-G$&xyY5F`>craf_ z;k;mrC2&{n{HMzD7Gz}-uf2(ql2Y0`Pv9JOue&QEc9R9~$9qmr&Lx~&xiqi+iXOMX z^P{OU-Bw_PhuxyGcMn%l?mSomEMGJ`af5$NS~S`>*IPj{}BY z93z_!CREsM^y}X5sy)QVNYjWk&!TnIn3I8;7+$jSpPOt`00x9df(&Urw{5ZUv+aYd7KV$ znosZlt}whhUd5%Cz1vl5)$G5e&P2%TJRO8#UtX%)u5;^c$_yPr$UQ6oXGgr8`Wf>Q zIMX~YW&uc^bx=ImfP6HO8H@hzY^Ppn)R)L<4&o>Pi{hQ@$@+`ZpP8AN?bVEKI>JJ< zEkY1>lR@=nw{sBBQjh(itbNyBwj5ksaVOLvItE4@1Mj3v>li)S=`*cNM_L$2UZ|CR z{ArUxlnXnBv9U3Q=UEgt{3tM(-C7uTsd|Ml>T8SN<+-`JT_J=p%!8;FtLqQH+e96R zV>xYEH9o4bS#9s3@V-M4xcYTaw|w~4QLL)Awl?;K+5;@9cI3SI+%NVsIp-lJg9)}E z8Ru)YRa7|j+I(2FYMaidwOE_a6fG?eK%`i;ye4$<^}gTJjen_~h+|V-T|Jn>f7$yy z!U$9=l=jy9`PN0Jc}wRdSJM`ObbO~i+IVK&mb>#A-N!$Kt?I=rFpC>TyqykhR8U`C zM#@RH*c$wCx_IQ}e0jLIv9aNc4E_B1vnEKC0oGQJWzS1aE-qCS6?{wi?Q=NcAQ1U$ zhmA)M?Sw5h^Okkm4rA{FDZE@-x>2@fhPh@$UF_k5(Q@*QlJ221eWiJW@%h zSurDVK@COkLkM<0S^+HD-Q86wQZ=?teCzJy=y(oLtKQ@KBvfX;){=zV>Ld6o;FyP< zytKqv@B;jM#Z`qxMUQ8B0@o483-%+|m&9BazsZ~zRu8}k+;sXmByP!g*8@IRxNv_jo{Od zey(9w<+hPDfgd*i{FzO)RJXOIq2Z>q@o-#J^tLbaeaJ~l8of*+jJq7^cN1}H85$ab zH+>%Q=6l(fyyoU+aCe-R72~x0-q*Z-McLY5QG>yR*YwFY?Ya*q@h)$y<~(k8^5ouK zuTK^zt^IWwLvG+`y&kd_iy+tjcfADx2di@6Oeq51Ev-XBx12G5JD4_nPcDeLtrq@t zg$yNzhllr&yYGV-y8~X2yk{15NHnI`|;OtMbo1lGRoV)c4$&>6%&%#&4LKg;j(T1`9{31 z7l0nq{e-`?^=+(}+Q;wXT!Iz)H5xi$zAR|Q;z{*5>D!lylAzhj-AZ*Y<@0XSziE{fU+FF*v&AP?l3eo1W@`4Mm0iMTYh_7mVker zn`tq5NpmK2_dk=3UkmKMC~L9<&zrq{I`$>b)9a;M>JLyAgXvSv=4+>Kti5lwYRu1f zr!@CK{zp9*hjE)++hF<}5BfmBBb=wErXH`;9(PJ+dwfLVkCCOYcurE@qc`EF4M|S)MxaNoDJSffqb-9h9nc||#35+f;NBKMKJ8{GzX6EKEreD$CDr@}d zbLx$x0@cdnvtZ02xN``Eh)k8S)bT7}yjl zC?X<49hF~H#2zN~hLLENKG3|he)H#A`0Z)hV-tlO&iiG(or^#GPQmfda*}f{x2tFY zI9qqG^yY!8lapgC*OT$ey{5XF)NxXQmn1=9`r>fKi-m*@tSN8Z zQyfR#BGnT3S0DrppksB`%MbUrWg1nv>K($8Z)VY`$i46Wg8V%Ef&x1>3ItZY+Wnqz zFFHi;MCnxj(z#vOddF?p9SX|yp8e*a1^`MxdN7B}GB9Yp2NYYQp2StSBP|*!XM|gb~@>V<4Gks&yzlS+F_iQ zyvk+0inkX!!FkJyi3jnp45+*(vZ0Co0CDF-PMtLeQ3fjG=ZonU&s*7K9vpXf8-eKF zO@7NClhY*R5kdJq2>|OTu@g8xDOLnPURf>tyguEs2BhwIf5rQL`ep!~NNC0sxH|^eQ*2iC_U+H* zwl4@dF(5a4>mAp+!ytCkPk@fowQfFF1;D7SowDPlI0^W{M!o&kySw9#iCmc%(_D3L z`DO!zGl1?n8<%3`#DX#DqSA)XyORZ==w#HNu+Hg~xh_9o%>fU(p9$e8IIqhQ^$!ft zODB{A5diota@+M*8rBNnQZ9fl9kxC`3?*@UADHX1yT`gHGFe=oY-Gr91AJtjwn-!P zdxk`t<|Xy1R`;D2^=|)#7e=^+a1ekEmyP~d`>kP?X|AsQ^I`s@!8CzZvnoYQE7_WZ zg9DEOPM*Dt-%+JoWY;r5~J(Q&nppRBrwpPt(Q zP^w#UGpEG9`WxZ>vAARnoR17}8xVAV!EZM#%3c2caon5USTNcj=(_;H)eO$S1>*73 zL~gTrq&^7AByQ{B`klW$6ezEp_J8|cm@{;=c*u)RI>y+gzQ&TlirGr@)+|E-)S=yFgf|QXGG?<<7>rdrcb2#QN zVPX3dge~Of=`jE8<vfp7;LrAD(IK7Xhl{#t0uNqjb)6FR*z~;b7naVF)(WwSPhG0E{U7N@(+CK&w_$aAPzt_ zGu8%nR2IWR&HM3T83?k&mfJ%>?A?==K)k?&W97h0_w9^8G*Y;n6pL6z5N52VLx~## zDKHY^eLKGl1O<=F@k%CCUfSGz4<^k4VG%eTWPgZK($dlbV(e%Hi2vfMVIY*?G5>&Y z%khv$a-z=^DLp&I@ac=DN066Hahm%CC>XpAV7M4WY@&?#sEEVwZY``YpUO!F2zZWa zJ_2tyU2i586&B{ortFrm-Z#Z$oN>F%eafSK8jNs8Ti+=asyICEeg=v~U6nv%KI9pw zxGUIvS_6nzMZni%QvIr)are zu(Emsu>#;Y5lFYwJc0Y25A-Q`5j}a*KkQ%8vdz?j_s1k=j`c^56q_X*HsGBUEG zmRl=ehu1x%r$D3@wuNw50SycwNZ_RR`N-SLg(vjMlsR|n(Q@yAG&Hy!gL7C4WnIL5 z__erNa9xR^&0e^=L$uUk!;@S^q)-P7-yizdaGh&^VT$hl=KPBw<HQ$=&9G+ z8Jn7JeacI@0EO_!TFljLPNxYHz{aL_K!QQ$OA3rYp1pN*kdYZ`aNLuA#DfS2fiyS* zH2}srRa9b~e?J}J>K^faIECc_kWR|WEuI2bE51+?E@c&!w;*@`@2#H6Jv&tl7++o~ zsaxU$>HB{^epjbBx9(0PI(fbU<&g#FHXexExWkD8?)q6tiv)D3^u00NgE0|uj1O_G{CWhUl&U^t=6<4c)2q>7z zY{vaSN;-frjvG(MQ~PM!<$Dl@DM<^)_gC;=g_4hXp z+oM3x8jNMWU;5B^%o+~*B$#A;XW7xR{q;4l%!fqEI+OwJuj1X~FI8D+(h#aX6w3Wi&24BnqtpG=Qcgl`~gI@y* zPpOU9q5g7yaL~n~*I}k2HG8MR*lcP8I{Uk1?oiwVM%Dp_*TcQJ24t1BP3lF8t_6Y4 z0`(8=pRTTXdCU)$j!ZbXTu}`FYrAHO>|rC40$}n6{(&R|70@;3#0y|Y3&?ojk0!70 zpo@j|<{09WeER89Vrie|PZuFt-C$45CrFSeH7^MsOL6Z~Au*p1JU%IJKX+uFFpc}h z-FDG*jpx?OUTNx! zFHmU{fB+Jut^i6hE-vmvG@a*;lrB@Pb6xxxzai^4!pt!@}L%4(Z%q?AQ^czMtTUCtDU4zL_n z4A{uJVa;}j^w*1&)F=G;a46?4ChuY-XueyYjW!ct1~!+c*Vax`kRm=09J${tp6jKt z9%`<*B^F~zz8%u|ZI4taD?C$!pAbBKJks_wq>DOAqPvLpUxPmr;R_r}tZ37f@M;BD zlUbHv(oPk+-AyAr}4OvlK3(8)V*CGfx;7?u9nAf z<&2d+dTgO;gjk0&@EQK7OrHdeK_)H)DuZq=o+Bw6RB4NWNuBw!XfN~^m`r$K@R(t( z5ArZy6Md6m`yTy-v&sCSbbsl$C*%;5UxkXUHeu9I20U(na-C?)hNUysc-Wx_8vIn?goGS%Kl{9(#+@VIGEe8?ncwIpdXpYgqz#M( z#C%4U_4!W{J#H?gzebM3!9Q^~jZMuhTjoHVwiPl(SnoM!bblMZ4(M*i|r&F1Vo zCS%SqU&-Xu)GsnO5pLR{z<`>r3;O?p@9i%iU$nk(E=aFUJ&}a)bliX@{fb_7loAi= zw5FCL{wA(c1t!tUvLk2piu?cG%53D-N_vxBE=g{YKll-DTHpxT($-^ATY{`G58_0E z{<)Jmh3L)qkMO`_nX8u0h7$kpXR&NBF#B0d8{pcqNldn(i!pCdOgRmLcf5soIk}n@ z_&<*?SP!3wZS-<87vY47p*>{%qSWW- z=a=1w1G5RCjU7ry-O4|+?AE2iF|Pmel~-ZG>~cWm9Zsp^ks&zXOKG^`q0KH?Y~H^Z zW7hgKgn)%uz{{h;WHJQUr_12AJ!k5X;({m%FSmjM-CA!dqpDZau+}i|4D&e?nBO6{ zfpVijSPh1Hsc8jLeW}4m*vM$txkzvPScBcLz`GGgTR~o3^1fdDExtJseZ9v60U!K& zjS`qUGt&+kEwIpxTDRfL{n__3Ul1wIOA5tQ=ZY9S3|&&(p~*xj6Jht6EiEkoO$I5G zB6b)$L;uD13Ge6^;h9zMyqk=`w(r>e{=T8#pA&}>Y>oGT@i4 zL9j!*(A1{?)d@!&q=qokAadGd-#E5cE87PNR-|@FY?u=YIi;oHj{o2$=0GqTS#*&Q zAzUYHM~pCvb?x{wH+2;1-oKG%zC$3>ZSQqc;BJ?^b%#&Zr9s13j$g-Wy8tGEE$kTz zTot8^{O@?GzkGH5@Sp{n58qw>`WwjPJL|WNNdX!JI_mIH@LUhzVen{79!XeMU6UpY zu10R_TL)q?Ad@yL|LZGiw+wvBB?*)q4t-9FndC1))Bc~`+>$(xE;7x7 zijps0P*n8dRl3NS>)UO?7k3Ebnp^XtZ~I`J;`mNlbP|b0rzv5}G}DU4G_ykd8r6}o zcxBtspyJs?*vpr?*U~(sn1LP4e_(#4W>RQzgHGF?Nnr4}r2uK@k(6NNfOCyZ%DIYX zs!RpDWzPBL+lvs+466V*@gqgbMM#@N)*i!~6^4-ijelV_evjrXtKTp-h4*YLmc8)5 za&4^FHWq?+nEXd(N=RG$3w1br(tjAm@b?dTSE4*OF2l5s8>*A+PPjBqV7zgV0^DK@ zV!GHgrKcWEIJ^wZV%ifK_tQ+%oVIVqmVEI^o8kHM9CRPTnE)3t{<)A%LgX?{2i`IB zCBvF|VJXqn`<9>NclrL9@W8yVd(Cx_076C9Y+NTgM(N_iK2lVS@cJwG$Y4hqo;~KH zZilQq1HxGH{IgTkE&l_8CL$H(VneT6AA=znq~@FVU3s-csLkA7nxRNA6BIRHBtcWi zYK(aw-sPE#k=?});c&YlAd|k88Fml}D~~FSkt8-|eyOv)3jenY zfCCR9fwX&DBc}6n`W$D^y+L7`${^eHZNSuFJZH$6v4SzT{p4|Nm-qj|dopbO)~m(2 zp!4BvVxImd!~OGT{YXB)mr4Bne}6~D!Glmn5*ev$6o{oJh+tGgIH=*E3_Js+pO%{; zn&ul^sy<=|Kkd`u(l0{o+aFc64DO=d4x&7>KT`!^5`rWWFv-d7j@7aHYJj_eP^nX4 zW+B-gj26MQI|Se^?l;o&+VTwj_s?kzq)12}mMFi%!x)u)d5N1@7%K6pMp%#!I} zygtjY`UWS?+U1Zn54c4{mqD2N*P42gV+>i~+{v(Q6pE)XY?|`IzMtZVc_JZ7(ZY>y zst!-rTfTtTA&Nz}cHD!z)P(mQ0TUve-9;+a!fACaHdi^D&B>9D1oSD{&FJv#&$6^UzmKvn$J(`gW^*P6j4k6|_SRZ|;+vj~xq;B8eAYy1Yj_ks z47G_W{Gm{5?(R5FE@o>yI5_l3Y){(k`JPTZGgRHuRox3z-HQs=rqGUp6`P3d7t6Sx5Ya0`7dmG;Om>UZG=9}@Eas9A@Em2yEzGcZa0qW`R($AKQ;%I#6PakQ znn}q>`o5{JZGXDnKW*Pr4GI)w%&dBJ7?y#Pz6nI?L%Of`5|j3Jch|R9s!hGv4x9yW zJZYJbZY2E>nGoRxQBmQI1$!3|zrqWB@evhle~Rj2J_tpH2vM5dR0$d_!)w&N85+b% zPI4fGhmUh-m;3B`d8p)G2f@a3(^llB-EhT6eS)~ijgQBGn;%Ix^l6`@)%m!z51~%| zq^f~&D=}E_LV390vt4yoX&{`qTr~UI@d(a9>#4JDaPRcB+|b`Lg$m5uos{c2T;S8O zjMyWr$ecTUwxl{W5mWK%5}CsX(PZpIE*C-{b>`g1*hwwCukai2RJx@&UvYKRtuk?J zsxStiY;0`kr}DXWXhzFM(=2jH%D-22YuOB=l<#nkktjBG;OAqPa@XJEqyDrUWLXcO zG=?{Z-*{>)tc>=$_w(IVSYMqEr8vr73(xG``*J*_+`_*4)CN2NIPx76?CU*ycih&v zflJ$PCsQlsT!ApiLVvoK*x5jr>=;+ii**^NKErX)I5V{~1npsPHIhHWHSmcsjUT8Q zfZL9UzM06z{haXeIb||&Sh`kX6prcD9`hh7na&6Q)?Q(g#p1K1MxDCCCd*1J^I%fm zpjbEkr6NTNy7f*w?sFg9A;g934n_euctHtD8hJW-WCf~DNRjP5NwoFt*WxGBDQaqN z<3fu#2;t=Uf@(-{)@KkTub}-LVOSJ!1{3POsxyF|eq*r+dbrVXHJGiA_hJ%96xID= zSLZ8+jrzn@2MMwmGr9L&HYdP&mI5(5iMaQS=lMzqJ@e|MypggIqTM2!B=16?LB2}< zXa~vdb4MvTLB-%Flqs257D{F+)aM^{q?Y%hY`$X-?CCzo#$!NW^2^6Ih@ATQX>8p( zwN)I6%&&Wd8My+hMiO4C-H0VP!WrRy$IA{%ZXoQun)|D_|o@^SwFhUfog zjBayu2?w#3aWS`CrO&2b=}!hk>v@4A{O?kSD$5pa_OAvF57&y zvmqU*#De8Ww~-XyK_@85vZ|bPpd|xAYC(!@O=N>P-;#a7dpi9mEt&NvRl} zms|NHyO7sUfbjSxsqOO8{K=lV=l+({j*6|VWbf>5SsFI>aQKc2zDYE1fCHu}0f%&k zs7Z>yVvBJG1eNiwq}p}G;Taqx+SaTr>S8g8%OpZZ(%MdZS-yT(+;8R+k}L&5S-PQ& zaBEs*yegkR$=pFT^}>Z>+cdTh+oA_Yc4S9MfQMaak#QUYaWU;Q#B*nt60}$ktUs12ZE!K6`7&re0P?ja=8GmP zkwux7dv6YzAkcfKH#OZ!k6qdLMAf%T_-7_sWgoT)<`e&9lV~Dn^1z`$RxcD^rxg+Hn2sBSLPVq?rYDKf^Avu?Y*DUD@}6cn$Y3tlwdFqUK#obQ`dF6}cK zcda27Mm0OYk3dB&HlHz}Pjm$8CA3gk5PrvED#>zc6t3+i;++nO zsRbrT(Lqz%A{~=7ySGrAd+xhIzmj;Y>t`gOb2GA{G$k9 zN&ah|y^V;2V;i$hzdks?Ap}pfZ!Dm7Em*@QzQkH}mfb0c zfUMY-zYqdE0fGP02AZPNb%kXYl2ni!mB)vBuyS{-_sZ-Ai6HpHP%aqdTQB^?@kyzU zm4!A{6u3rD7iMKCnOlk`bgp=)Vt*y`06e6m z=6kyzouIX9Yf5McbLt6wWZt9-`?Q)DC2!kH0d&;RTP99 zS58A^fDU*#x)(2VVP{Fps;(fz7%Xwrldxc0)L3;HBlaz%SORBN4 z+Y5_}C#}k&T5GjUk= zG)MnxOFMK&jKri9IxxgWL%C?yo@QkvN^+b-@@dZ>`#2NIXuLfi*hR6ds-dxzCr9g3 zS;>a`_H0iLY2~+}z!o`tpbELk{DnS#Alx$@&W}QH^74Udq*lZp>ZhyWSC~Nhu{x_) zxrJ#U==H2>3BUzlk~Nsm$X3-gp=t=gLR^>zkK>+KoETW>#~4>lo_k4$ygF? z?dVq4|HH0J;3P0!_b1kQzu%IPmRkKzzy+s$oSiYLgr}LTGk(gH4yqtJ)!|zMLcS8S z0j^)eE85Qx!c!@?erM`E#w<}SuT+BdnE%$wG5z*W!X|THl;W|rtmxu~i9N@X#_eF? zW%ltAgrK6T+t7bKca*9~X6`6hJl^y^x0)RbaE6vqQ6@h{uX-=JLn7$lz7{8xhe77i zFMp-1avo+KBKOAi>sPk=0BD|}1iqrLIa5Bi@COvf9u&5L*`BVBU%C2V%9NrbGs=BpMZBbsvHB3_fc) zHZNb{cHP=8*|NurV_M1rB)IL##L!R?gz+Js@~P15d52s!D(c_spsDSP0)24iV>U5rxKxs6TtczJpiO z-At`s<)vTbIJ{o}OC~0!?|s=CTykm(8b73IVyAT$43L;j%N5}u!A9*wUI^vPMOj%L z7=v(vjF#Th8PoKus+Oqw0b4e>XjdIiw!=O|TdAw%#ap+<2cXs|eG~lbpy*5xuly;n zkB255GH}oU&wb<*fc?kj2_k?nfZOchFD1S<@LtI6Y@EmtB$e%<;ovwbf!@+kiMj~& z?$gY%eAJ-r=!p#*?nTsZ$_D|o9v=edS>EU7krV_T3K_el;XOj>C~y?fc1)mO^aLdt zb2y;yQtK~z!NAW6j3&vv)QyebQ(}Yl=tr}g9zl#}L2&nDWdb)UnVC=Vg%EvFKX0e} zuE*tf*4vWZ|NLvS+$NsO&ax=0EZJNXwO~z7RZTz z5ou)m3=A1ksj{9eyt<^FMcg;OFV^Pir5MI_OW37>cJXg)f@KFPd5RSRolV6seap ztHU8eXdE()?a?6vm0y2RLKuzbYiq%VgCt`%{wu?%mse$KBVH%)RQ}lQmGCw<*{GjB zNNHkk@9)PbCjR2|vw2uk@AX12pYIxVyb{4NKzzf-nt zcuv`gPt5AVJ0{D_bjbooeXtc*ey2ZB>hJPg#WwDN%GX@sjYj3oijUbLBsz((zPGFE z06VlG8b%vj`fBcx*xs%|5lPpv4{ES-8eY{3J`$>6%qUA2b9VpXVxnJY!gJ5QY!XE@ zDi5SvVuzD!dIlX0*A) zft5Wb6BA81`n0N?QTJF5=$0=17VwXxUs-8|gYmQbQk#a?2RD1vTT{VVz6R=oeuH@B zL^}a9e`y{q7<*%F+q6gdrW9!LEwjmK{(L}HP$johO+vTVeS)L{Gq@~IE(NF)0SF&dkjo12Q~0q+)v zW13z>Pfx3^PMiGf2!eeB-3be7RO!MSUsO`C2FjoqiG3_!;JCTUyUAe z4&4L&A`%hEF^aL=P_xIhX_uy|Ebyy}eQEZIp8Gr|ol9HeG)X}h+}i!aZHZ3YVV{(_U<#+YBTPgD2x5A*6Fes64+*9mK7PK%Cgi^^ zq&|3x5*Zf!6@#t~q3X{qPw)h-rE?u+$E+lx%8Z_)ESp%1pZ<7JClKP01r1(z7rEm{ z5wq~~$;ak-nY|nVlqB^t8wW$u@qkc|wa^n?R2YF1nkX-)5^vD2_f$m7Ceac6@^l<3 zQXdF!r_Jq}Dfkeu&) z1+j{wksa{U{;{tL{KsWoG8VVtAn9rih9Qa0M~kK&&)%P6dXojti%FPm-tvaCZl0^AGEQ2%L7^CSI% zJT`JUh(zZa4vN-C7!Ab9#%?nIkOx;Z0rM**d03q;sim_ zu;O17ko4vFIV42Cq;fOCHrY8H?DQJdS~jIYVUZHKLg)v`ILD|De^v3Ryg6!yTK75K zyvEw1sleGlh2pw|s9%z8!a_$jNfFxww4;xw8XG?U$+qK%VKVpL9JHkTo)siZ)|h({ zZga5LQAzTz;pc^?GM{(8%*b@XvWQ(!WG-H)q1vFIeK{wQ6}9aUuq#a$U2B;XM0IuG z1({o#Zvgv>07IE_2G2^961gouw%QJCPO~IwIq(0jH|zl`iNnKyGNl0ywPtHdtqw5P zl0MprElQf^d$7^CYJWDpWPy?1PhyR==5Gg8O!Foxo&0OTE`Tb4FDW@WInYIa;QuuL zL5tBCjiY*8^OHU4*2b+A6-T;$FiTw&KMyj3d=&J+jxcrD)N?RVotaJy3C!tL z&A34``J}TUH`$li4h=t3g!)#X@6&YnCr{^ZY+qL1<&oGN#ooqmdjKYG(tDFs;|@&= zrLU0~KyTWS_@3re+w!!y$*j@4RrC!U1ioMOVH~S!Ma3q;0Q;wT3YR8=#F0gGz`B zfFOTF3(=!FnJb;lMdUE5p~Z}D&=#Y{v7u}d0qxv62kZS72@ZI@Uy)Rh-NYm?bS-A7 zOIzxDU~K+T%z_g6aSzic+vmpF;q4X<4fXC)C2PKm0X^S3F&{s;-PR9JghkmYJN zXff;HPoIz$Z0v9{6GXjy`y}tn@zD`^@%w^q%%hC+*nZ{cvytZ=_2O319sL;)lHD*& zrAs*2p5p1c((HjNo4_alRVjg%3-3xbIs%Ny-+kHT&TPi~v#t2B{-)XEH?gcJ2}FdP ze9WLoK~&k*fL5oD0ZwVJ3c{U$49=L|6kC^AJTwXTxFf}nd2x=W@1eLW9r`NXOM@p+`ZrX@x}q_=X?gm+Vc_i=Qkp!6Jt9!e%@*-~6{#u<>kczsUfGGAg zi6a5{oK;!T@2a-xoDS1Mi^yZOwL7}9!W|Eb!=ZEr`AE>$osuQQpYcnL1*^g=kw|`4 zreQKD-><4DZp%1*KEboB#E6o7xzbI`S0lAaxBOKn`q(Wi+YTzQN=oJ8CiSO8Xm%dK zkV^D1A7?aJPX&RPe7})!XG!Fea zg@zreu@m6@A#HRxP5z~ z6pwvYLIoFEiyiCct61tS@~R}B$6(@30DPNq}{0UV)j#8yn;8=WRap3oSb|)ZS z+^F<5IVs+Gol6w z5g@+$-7dUt_;~cvC@c((vcg0D_>|=nRj4b)LxKMN(e}Nc8|#-ZPkqie`o|Fxg$E-< za>V`!LN*1rE|$M9^izlXhJl!QuH!*vSjt2F+(BFGuHXUorC7j)$b-5JK&pD?JTnMR z7Zjvi0sQziPd16Z5oYsh<0Wg1ok9u@wTmZplgtBihw|s%!4jrveKgwj4^+0_wchH( zhlip0E}TG*jbc6+j(tU0oMe2J`uy;H_(NSHGBn#arDz&8AlC7PSlwCrqs zh>fis9qwL8xHbNj)_P8?>@CJpe`sCtM7;fy6r5HE{CBH?^zs|a@;df4o_yFOfV4E1 z+r8O@C{eF(#DepUA?~9-Y?I*t>=^Q?V?Srcd0Ow5`%}cat&el?y`3tMJi2N)a*VH0 zJihp!$RuQXa<2}cd!Xmu38g;6qYqRJJkf14UvBo6;Yz8|3%W}>DH%!6;GP99K5I-| z_ShJc)PFQ9A3Ui{&)8Rof+YLNcB!R=%Ab9yaC{g`5CI+_Vl@-wG-($vc(PMYTZvQ-;wo!9n%GM z*11ibN*xWjorPT~vJHP}^K3d?o2`()U!IOo+R;8=szj{zLC$tM!9+(zqe-FCZr%^@ zc6*Z!s}9bAd0brSV7vCQ-`(JH!(6Ue=pa9sDK0Uzu(X~Cl87Tx+>(j|TQM6W4-w0% zP?yHQO8@vRGRaE)P8(k7GSvu7oRxdvD_}Hjs$Qem(ZHcT=1bUj_1T+WJ0{<-|GO=oD-{?LkRjcp^Dy zUW;>WHt^Q-hEi(9Po_X$^j=!!eUYNI6W@!6X6l0-h|7pB&rEOJMK~g&UZeQgH=gP$ zO5wI(s%AcNus>`upSTrNy$pn69H`4{hsTrqv6Jp#|0sFSutI(K{9pYLoNmiJ3?gZ7 zT<*x=cDU)Vc&K24rg&Ks#KlkuW+Ta?WNuzrM`?Yb1wj;|q? z$+#s(`3}2@Fr8BJUWug1498Sx){Eng&b^f_5)Sw1kTI=3TAT7gw9o^yUS6cblT5U> zqJ8XwnL;?Y5=x_8eFYP-SdQGXZwD{uiN`0$%+!U|H4(;YEu;1g#~!->_g_6Qgt3=z z>nmbZh)QgS^&1hnHNr#SZ2cy+QvRU49({_yg7Lk69Ljym5NUN)78e`+J$u^JkBmu8 zFFkp%{Xwabhl@cDm2b`Ar}!uuHg??b8{!2MtF>yW%VgH)Sf;l)1Y9p54PD(hKc0tY zPf1=G!XId<`nVDi%ZlSq{Gu`U!vuxL)#CjwwjvLLKFt@doQK0{tB>0?a1hf-%xK@X z%fmZsF5SV^k<`1WZ*|=ZS}kz8wAgshU-f;ubPaK?4c;qKIH1ErG$MHqu`IA`;Wcx__8wWzP@ZD8@R0^VfuybOTj5`mWJu zhbIT;=c9-IZ#|+T5==1o$&)N-RVuF@CVu`c~{F2;$41reMI zab;G1%k*I(=Q#;T7uGj!)Nja+rgKnAuPGmNBUz2$v9=$_d8So4J=6s*s(4rrxo*EF zm(|6GlM)d^%=iNK$b8lW2Qa2!c@3t)Rc0J$=;dEn(CWv-JkCsVxL=&R1>Q?Ge%_yC zaSAUCYmXn@{QU8gwIH<3dFY6<_fHPVsLwdxPAo*F#^j zQj*@VAPNrcy($y9{uQ~7;u}qY@y;}(TNOz#RmXj zd zg_;u*2s&IzX@Oj0anp-w#e{dkj zovcIN9>%pI_M}#AXJw*r;s0amE5oAdqPBw1PTJBTE1s6Gz`^bIHs=$t=RZ3Wn+AH+5Lg#d z#4e5A2SKeghvH{zH`|OsYs;p5$bU}D3oO7y2vr=9HvJqB_dRA@;A|4%3RWYN66j=9 zI}5ih2m{+YUY|=6nE?>6>+@=7s3>`z1T??@@Yh>+EMgq2DZ=ONG_q z&Ej#qba)MK;jpJxTv}5NHoRH-fl2dRz|Ue!U%yAhq4n_P716KcAhb zFVz!-Hb2lte4o){#1~M-)V3`}68%yJ3;+Ml?cV{tCfkSCk?HvI@fN-u$-)NyMU zL@Wv;eFA&C+(~U>^F5J9U|dDUiE%lWJPDSa;JS)7_q$H%^ZOH2vIEth&Syz@rm~8K zC@-Zi`o__p(!3#Fg*&Ybzq}=hmZo*XfQsZ>Q}=v<)lw^7ukqdwupH@5h6(qNOJjR*5>Bm@&5TM-KTnE~Sd9QuB6Kxq4aDVkgd%&y|U``HOQpDV`yOL`*AC#VSP zYoa1X7>JD6buzZ~bDGmOg<^D*CT`t7^p;5Ae zuWSdc@i`w7iKdSaNra!+(6QDEAigZWs0#K>{)TVELgF${uznxhz}ym$f7obn@Ezp- z6AZPu%3hk*#(>u(pVYMmXz}yO$b&uyV{*e^+}Ya&nYYM}Tl(g@xKfZmQpA$1A;D8j z3NlyE32Vl1j{yO?_rcqld4lo|VwF5D;49Vvb3TEwgQk8A$Y zEK=@CFyC*W&K;~Q#!`jx<*PSYFZ-YWxN-X-h-=E7)OuT%azM^mlk}9i|B!*{-CA5> z{NGD?f(PYg!!pp&Pj|+vhgVO4OaS~qwzUN2Ou~f)v&o+FwRG&aQ#Gh7YuPWFwZ zvIswU>;GM*ZI971<7|J_$qrnG#K71)uT3;bE5WQjr6(U4c5SFiQtjpz;hxxWZH%jHz1 zocHr`%;6RP{OQ?{R+w`hdKP_2WVQEK1V!^LeRMtA*i6HKj8wN{B6@4Af`gw@!dGge zyy!Y@Z=Eg#!&7F#LcyZK11;mb+c!ld)YLkXyD)3L`yJm!(CyV%16f6sY|GdKHKq5L zGLbPv-nA}B9%xVsO4D+bjzA9Xz;*r*NatN0Q9;a_sZmJ(q>+iBOP22~0&BKEbztwt zo!naZAv4S+-ajG7E|cUT*reiGGjH*n$Lymy`gP7(*4;I8*Zgxdoq3DkHoG;`_O)gz z%}~_X*x1+lE?8u=VXws+JfFy6Glj)1EhF|{oFRjvY?I`rgGq4a@}l&>`!=H0;C(`` zG^DHd=#He)e4-|!w6LW~@^o|42J0@Q9?Env8FIfuW=q{*$nSmCm%?8v)vb~IVvNKy z!sna$_b*RF$B}*oa4#FpQ`=V{n?M|Cja(qMXFG}tM zw&*HYf!)-4q_jOmWT1*+X_m|WEezo8K ze0BDyU78REV`=GfLJLUvXoetbirq5;Rg?Dm<|GMuC=n5nvC}aJpLUWuKkER482_X5?SiEW>%vKxWNE zM#a~qY3sv3zKAS5pYl=5y`-E9Mu%f<@&|)tuEQ0ql(I}*J+>DEzmM9r>w;pCopB^q26IZ zEE$g@Je69bp9pePF`;6RH47NQdpP1j>|NdToy$Z3v_H*M90Z~`_r&5eC^Bwva*Gvk zDO@e<4Kmj@8^2>Mdug4Ky~JqcN9Fb1Dk_v1@C+-ePOgH`I%JQFV!$#EF$RI4Oz<_$ zZbX>8%hR?Gfde-e-SP5d?jIN5V)&l6+^-AITdfVz6uZ>?GO=puC%ka+NS2&xlw<0d zh`S9>r8j$2AYqV%J3?bQb|llC4+8 zt}=&PsD^gUI;|Z;oT;b&@miVM149 zG-)-1)6ItKQlL2$3@oOxSGl9vRIFiu0jO1uk~n@jaC0^(KU)(mdxKam`zfi#41D)L zJzYSba{YV1mx0cSUTXKv#D4PuOD z8AG-VQO3dUH@dffL}FrzQV$kFh3p%=OQKh}c%={p#@vDS0s%Hm0j!Y=@GwFQ$O$q%K$7(bK{pi#(+*PMqjRi*|6rc(_;LHEgjaesZ6+1sO3I6SR! zK?SwmEV$ddcDfGm?MAlUcM1n|?s)reI6to)eW+(?@tbwsV#@+Cev}l4F?|P?T=_ea z@$q{N@H*$@=sF2B9k^*If3(z0=;tF{S6^#@cSP6NZ1I<970bXNzu`qp#<~*>T9LXX z^_F)l_|Bq&Y{J@cvG%pUHJt(@n`!FZ*~Qfq%U-UF4i9p6X+jTE$%>!YR z!|}b2AfH9aROs)-!(Txj1{V^?CpU?$_k+SW-bJ;&X^?SO_T8%InYCyoQb*_@191r_ z0okK5*05c7J0T#&5zC2$YYS8Gj7QXf-{_>>)0qnl!D@%wMV@Ma60*M9(>L|fOPH7> zS(ozl@fOQf*|F~IMhqs?ID};8*p}mZhUh2|ZLyaWn#i7vCi@7q>&)2+QBY8@mT4LO znQ;=y_`PT^LZ9Y%$G~=`>9ymLPjirkm7Q9=xTYw2 zShunjTit0?vt z_sO*P%#W)ylR<_PUqk_UMh;g%R&($YBz|O?EHywNgP)V#RdF7Dt9JN`q56j4Uf>ea$Rn>@EXt!1@=AK?x+)%#5O4ujDYCL%{}J%;Dt@eWvn4|_kF<}*+c%jy%82O?l; zkDQ}1wo0tMUzD^l14Lx!E#S@b!J#vojr!-?u~VS5ETmp-+-O0G`KaiRQ%7P#f&u>= zGMpI)_Qw^bTKO*wsa1U%e@OQ*E*PMfl2GQ09x+KFNcw0eVG!CKiI)YaqII52YjtSy zpUmE8-`DW74ZdoBI5MRbMg<9eS) zNw4_^$1I{|P}B$V!^luM%jsb!?DB=3i~|=|8nJkD{;G1{|7^q0T`T~B=!vQQ7d@DX znwFz4vjdwonS&R5(1^YBXSH^dZ_AHP@CQ0BC3f~HUFa%uV(f;=DCgBdk_wooWX3YV zB@VXA4w@0m;-0lfw>KMOwwjEknqt-;wO=rb&=iJBq;=$3E^P{D%XK~Nq>N_63(?$` zjbKy@Ouu^x0g(a-1+6*t{yuf%{>^^Rr}ehI8ov2J|Lf06~B%h3@(X3u#UPiE`e^PeGAGw^pB{;O9FQqQzZ3vztF#TKb;bLx8T8?*hD_P2@I z>jJ?eX+V|##?`dXNONaXml8y)1U4eeH};HfekpmJ!W~^+n7%D1oHqAFe}qeDA0bkN zEdztImfd^hs~$Htz{+M`YV2)GUv|wa1_sRM;ygOix%hA0euC2^9-7!bnG|{XZuo7y zFj=A1ee|Rgq~5e72CQv9#Is$O$<-Px_q$YQ`)U_TxwSK|mZg80tmQ$p|NRsO%PY}8 zC2Neq=pB^a?N>*yA_h{%eE3yKhvTjU8S<%?Sr;w zC9R~KI}-ABp;t}a7o0BPT5a#Pbk{2GzI$|K%)dfa_O|n!kC9I-m=Ye20U6PAQn1vp zTkvathS~-%I=U$3?Jl=RB=VK1htF%JLZ%Ka@nlT5TIG0R`chyEI+a(3gWLbu>Y>M5 za16ie!*#r)Uu#_qs6Q_wQo7oi$pf@`xf}`49Pb_oO1x8G9UOes=#Q2N>65guJWW=M zGm&|GcttnIc_zZ@Oahs+3gs4TyBlk1bfY?rpp;7KmbY6B)S2*}IsA9l{dO9j*(RK+ zKe5p)IdhAYbbt`&K)4Di{OTSuz~_qu;_^8M&8M3Zn4S}uQB6LhwnCLh?01D}9x6np zqRq8`1vWq1L8@{8djoSrKqd)1JfTXrg%mbPDwlx&7kJ=c&#bUz1QMwj6*Mp8qogmnoWXs2yT97T)}vG;xsOq0+lcv~-85 zedCGgdi+VA%~VQzIuhj{S&q2%B)iOtHLQ5_$HU;OEHE?WGqp@xIM(2K36H?1G2g?A3ziJBvMM%9A~_ zlRate{XXx$F^03_m6S{ih0Vb0mnWz#N_2jIOnRRANB#ed)EGzK zY`J7P&wPyy%sIAd5KJ9RXpjQ5D*Sb@&Za2l4=9!0y@tf8ya(*p{ZQ8 z zR{S$CG7^Ft)aBYxN^H#kZ84Z7c(ligkUrh=!K?FrFDAgNGjn_CS<&LoPOPqJT>Eq8LutkbX8PSl0rknSd((`&2ChZH zGrbd$-wPDv=e(y-y8MAz=ohb% z&Xr(9YC`bHr!D^gruJBhNPJ#H(8)#)@}jFRlJcdr^2x#sqXft*$df$Sq_Tb*g;9~T zuGu7wFtH&>(I?MB?u0Fxlol1_o~wa@|Gyc^3(_t!Xfp(V^l_H5lIxT-sqUlOLZgA# zmK6FK$dQ=gYj}xCCGVhyGo+YLrI$=HJHNgdq%ObO2zTarGxK5{)HAdO=oA9ayXH-~ zO}0U4K}Rn@M%5eNZW2H^hjM4_j?!)@G8*s6eBm<3t)Z}lfb$qy)l-YSSc6v{DakbV zirmZD(vt#{@rh?GUaVoS!Ec<^(MW;!u_`TBz4$w5^Jh!UPoQ7QLxJx)C-H2CU?O%> zYEf0n6%_n7sPBA{G%_;G9&?rB zHQiQpV6D=GRJp*e+T8!++Gst0XZEm3!e|u()iS&W3~hn-n+jUYU3vNOPHxl>Evi29 zt&7mH4A$+Na-eE3$rEfDZ+e#j=~wY9U3vJR%tQR1orU;CqUlE>06z4FFl19~*eVPB9c4g3E*SXnn7J+)`vRuUXW^_bTNZHj&CNEuf*mk=5 zNrXDn6TcK~s9D)|Sm)d6Hu&450BKOecYdiRru(}9Z@V8BFGQLcc5<9Gk)?NW-n zccewytO8q`)hbW5$!HC+9z!h6T{vd0@b0>c5{Jo3#Wl6IGs-|<^#8N~otxD+n^^Ox&^v8ZaX3H}x0DKhl& zm)Ir#iB;ckmOGl3k%3kk6PEBhZ^mh8aIk7_Z(LDq*Q3Y>Hqr176G%9pL0k8GiDmb0 zv@HVTdkTyd;F5kQ)h1JXj;KOVT5D=c<=fvp4Jq&fQk?i)_g1ezSAn$A4GD@O=$u;~ zw7hxdIc~-La<#dFY8siRnwDmQSLKu#dEu=h|M(jlAmUuXQcezqVb3QtesHXP0`_CG;ln-Pa;d06V85NQl32m z`G(JD@aRkj~)}&Uc`8{*kEcpO>IEnyvMQ#}t%@;y5#<4`M$ZWBQP6(zA?jRF!_|vO=EP(%; zaPdaz$#5LC6sBP=)x`}E@5y^4IP?S$O{@pj9`2aKb`6X+v!^9Q``qGGc?-VcnAR-U zjOFbzhB+lE4{F#$Xd55AD$lhjv0}BHOxG26Ht*S~f8o)bg=toyTh7Y}3D>!%X$ zErDqG*V#BinFuT{@CzC5VlnFYI<7S485p>5C&9LCs(?My2<$EcLZj~b$&9VI*LTb2io#95R+wlt@Q&U0g8wXv(NVIqj7$e!!bA#CggI8gP`UJ6 zR|+ui8=U-=?TQ=MkAH8Au?M8ZKr#@|4!&?nlFTqZf>7;A$*^?Ry&0F2YXmZ3N05tYwi9?77M2V$bDENJ;{1vzQw?d=PPgejH+ z#ASp_BZ>Ds|ET%0Fglgq*T=R14banD1O`e67hhZh2U73+>r1w3FT(_ruUl07{hkythSxd)En<5DSwRP zS#It@uNKRAkDdKoM5fsanAKdvE+bxca(sBvx4a8As?13sUZ*MpqYXWcw-5W`y7d99 zK}7iO3@Q?2TjMFgUD{uxQaA+lMO&qZxN1TtA+mn0!q~9yR9Tgy-09MkW zmJ)VeT}g{O?61n8va4@K?FxVIPDJb_X@ONadEgfm{wn4XF=Z?G`Cmm=56sFk^O>ra z-q)ut1?HG(B;y!3KQ>~ujAWHjm4q;_=dskCs*`HSCR!JASUDAN>?ULaunhFFG@b5> z02&e*VgBMU80dq)+jf%#p-o*PGc3}8V23O(XvEpdCBJCe1^7RfPvEAKei(Xg`TA9q z`)Rwe7-Usq{-M&@;ZhO%i?UBq9W){!_f-Dl8lE!bAsp!wK=53~SRD;N=Q<@25t4w! z7H1YmKYk!fZx8xOyiX-_olCp^Ct%M)k#_;zI(ozA{P=+WF`}d0FW`sD-%9Inm?UI; zc;5XDkn)=UYDm-6fNG2Db(ysOT9Lo)uyuokh&sYiu%i7T5NOG?@aJnHGtT*?vA7%V zg^5fXv-P-3Q0U%4-hOHoI^;0@G@G9W63-)0`Y!JgeZGi@_bvPjC!gXwaC*`B$w+#%7nM?+6ZBQ~@$SgxN%C zW?3x0_mn0?o%IjzSV#c|hcMu`di=cvvXj=3+5UcD8{e|}e(B~-Tf`(7vmpAekJF@= zE;2Abe{p+R*K)zAdz9s)kM=A)P2WQ_R)f$E-%{p7Hs`)APh+!u@fs!WlEC8UG>-zgLc2!VgIlxj z1vf~{;xMPmba$H1>-1Tv*a}e`mqs*^Fpb1Y&Zl5h+#AT0Yw|CoZ_|0#$=5=n>+JPGd(ZPg;>rj(+knaTeaNK1=9wj6+asZVION{2bKa-fl}uh za20khD!^w3Re!D-TH9#m$ctbavR?CVWtB=#bjf@Qq(yNv`t<*>cat69_*nlHNkR30vty(oY`j;8P=uTji0&76-igwBNdGxPkF;D;E93t5ZC|?2 z@)4B$6$`;yBaHZs%_Q-XU4>)ur=EDcVwl#oydn|8f`Ekz*n)R3#qDAJ-D9{5fT};=+iJ+ z6}ftE1&tPmW30ma)a##iS1Y0Juy<=|>=shW-JNSA3Jd_ZiLcRxa?z{8?4N zu~gq}@vmDE&@0aU62d06fY8KzBUD7OqY21I2 zxLoeU=$5e}5ke#ZZM&sY4PPzVRRmB8ppL~tP zAWUI?YK8X*(cKL!4J81#WZCob=c!qNWu;WDQRZdZ{1H1hOw8k;142nI0qDY(P%Z>Q zt|ab8n1L~a>&A>4H(#t$dY*exq9MVKjB!jJSolED$f|t?{!+vRGS7H5(O}dmg&f5* zFfh@EkPfO{Jt41wg4V>_=gI3`u(^*v-jUJHefa;1ZpBy!Uo6&aEFr&rP)^Th*Np*c zP=i!5of#am{98BnCN#yM|4ltG)0uJ7#8)K+P9k4E`;x%JR8kG&@u}AtZ4~gn;_VTr2B>fzM*dk_4x)q5v_dH*&vCom z%%kAgMEIQ?C3R3KoEyriHC5qxr{h1c2Qr(uYSw3AqrR_`26@7sK)lC%!+aI4*XoxY?`SJ+t31e z9BU5VUnDmCCEL*2MV?4e^=hy}JlcXA5J`2b1;sizzVKi?kJ~HZd5bmGcL&1n{}DiH zEl>{mcD-Z(8+4FRw$D|LIhx|)#77>~YkeLHG(XbP28SgKF74M~*|@ zl!L%&6-&F@%SxKZiVX*5g7!9u1c+43GJMSdy zz`p*B3t|E4pIMe6&yc8I&eeVyupI0A>GQ_k$s_mQv;?yw>$1dKo2%y@$qUlfvZ-32 zssuy>1ezr^a=K#F?hyH7&ukZ?VZxz&nVre1-p!=_t6@5DXA#n%i(EnRD5+Ct`sB-9 zDc8B7v5|(+#Jn~7Oo(WoKneG=*M#`N7Rh9%h&c1r_pGUuVz@wpB19c2y^$j?I()}@ zu2!-_!R&qG8aZK+aCN6uUw=rMuWTCa^bF{1je?-fzxxpXwfX^x)c&MzMB2ApTyjU3 z@;Jj1wpsUzEBQ=O8NDah;?oc_vivQxyco#8QLJpY%pUw4qNU2OY@fuuYKym@LHiV{b3o&{xpYgD< zHo_-cjs19g3~-m0D2$rP3PGj&bpQwRF8%nDT za{vtd8;C-60tO*Htgpt#XW-@m?|n=gp98}zk`Icihk+UzL&64 zt&Fp$;K)xjoPIHwesO@6y|_K|P+^f}Y~XExyAt9w0!x8jB~qwa>}OFwDq5s{2D5R? zeO)waB%Dr6pEs40x>AzMm9XU|_s%n909m8W*<<~ujk$RUL7X=}TXCd4Aw=z$D)yJx znBozAe`j{yf!^Dwx-@T-_7;gU7T`D}bm*mrQGm_kESU9A@NQ71rJEyCf8X8eoMal* z4K_bx*p1kR&{z+KFsVXvPS7wd5BW3Wdy#!GUQS$URaH3d#{zA24)A)N zf|0U_(fk)yamXkRm6)a<5z@xzbOnA+anBijF9@=+Jj)NWvgogzhOeo@qhhW5xLB7NaT86e$RT3+LZCGXrj%WxhB0SFh^LHpn!?UjVf#0wE``` znj?_-j8qjJ3b5y|?>l8dkD4AoZ#x5JnL>&V{6Ancto2>Y1Q!Ac)PK@~*;rPyK2y!NbW(FZ(`kk9IUIA7(kdcBe0sy%}tHa2HxI&&GYD84`Gf# zzG>q5f~$dWlBiTV-xM^^5BHiBbYd2Ura_lyPMvdjJWME3uA;>pQL`Bj;qg_+$JCRGl&J|<#)DcW zS!WjDiRzy%aw<|f4DlJ(6FQ2pNY6+Kyo)}p>A82CtGyG^{H*y(BQQsOuLueR7p@ON z!@vEibrS_NzAs8t)OpWQWY=)=a1n5axN{3XB< zVS-ABts3K+h7O-T*N7`%vB*ln0j?5!4q#o$wkQHEfO@4BasMN0{8+n&?P`}IV$E8x zAp9lVojzh3#xWmurHxS-YuGk|ra!jSk4F$vqqmaG5r9Ob*LNd;<{Bmd+xT*@5T_>y zlJ;H{B-^q#`SMrG7^B4C2FY6;c|2@=3S63j1qiQZ#Gv`{zN@m>GcN$R*j~cI{pO1h zG)%NM7Z*VNR9pB&RsRf2&< zk;0Z4wgsqC@)NXBb94^@e(Egute+1fGjQgv+ZB}g^f?0k+xc?6jp3{;=^*-2ePA33 z5uD&HbI4$rD_KcV{wE}-u*dO8H84&40WpEBeOe2Uf>Wdv`L9`bMU=6>ytmpe=Kj`+ zMg&|hMxiGTv~a%b{_ZTTfwIO{&>tK9nhx@PnpR4#?LOfosXtG_@b~KI;w)FGk;Js! zvwN6@lVlX~c)x(-*nBaxJrGCxGZue0vIg7#fKHRn_7rpgbO-ky6b^FCuZj4%3IdXY zMEe#UfzP9Pp0xSjTYu||9k3GgQzVZ7!ZW#@ZNGW=#n~DFYy)e^LjKhz0%d26fMKuz zqjk9|9C7eEBIT!&l!3L7#d_k$533`k#pgWw5&^FWcx==wz(Hg)@XHi$T2ZG{@_!Xz z-{`)=%=wv1Oq@p&b}tUbjT;<32>{vTyzR0y{oGMAsFMZta^|Cq>NWP2|2r%eK9w1U zPd~$cbiM73NXsRFstq11xmeQe`HP;uK8g$Z({%yJbo3iXBzyCYq!UyraM@6Ge zKg2pCYcSh5+cAS7py6nQw&(1zD|NPYvK%F`1t?#5oKH>G5gaF9)2jB>}Jm^;NK#MtP2oKS!9)|T8 z?r50EH{Kk2wO?-z1Mt}q>>@pSyZ4Rv4HF8G0IO3+y+VIOu$c@${6lE?N}9j7JpLu5 z{>GKR1K^tC0j`PU)fia~m6%1D+R``_wZ;S2LFTEimt~_hM**k8FKo9{3fT!XS z>6y6te48OJBL$Ms-=2O=8wC^7Xa!2t|$rjxC4vSjT2s=&0Q-s##`Gm(y{3$6zq#t7UCS zErdBS#o9KXhW{}QY&bd!A(V;sl^s5BGY8hz4VStV03D#i>(`G>LwlP=!}#t~E5=Z| z@5|qyg4F*@EF;&vRxz1tsK!I6nTHI*$|69ov?GCQ(9)J$3ABH~TiqU>xkVwdt%7_E zOp?2?=A#U>$E<2wxReiykRM! z{Z$httc+MT1KfdU1JAbkHu`iil~4RjK*0N+3vIw)7zmfwSnYBE;T_8a0K53aykAk3 zZ+YhJ)+PfH8l>YPewBvG739_&>>rumG3msPN@IeyK2|izeKPlAMi;=`i`ej5{XQd$ zNc1M3uyDG|y>S|GtYwd9gpzG6VL$=%OajvRw7MIqZwA8CQJIvYXfD0y1a^% z3gnh!7fD7epuj}SzcoLfDRS>%VGw0hd~FUHznG1hfR=#)O&A72Qp0yX-2--4&d6NFlDUT(joDQZO-WAN+8K`7TlX7(#LIWDo_-;*$Z? zcg(_3MNl_@$ZR_7=q2=yD&V$$IR^g@wTwb06m}K>v7p)E!P}9@V3&m)`P(u_P*+MA zZUe}-=U-`a?n3pdimnHFA70VUGuS=|-=n^li2u-j*#ZS9N-NZ-=*=3o|g(;fo& z=h@L!Z~XTy5G0`XaH$tao`V~iv(Std}ivmY{p`)6dgy?;h?PYVNS_&e=%?#&^nA zkm9FfW)K<|36$W$GggAf&yBc*SIru4a!t9Su>tTvFh|N7xZie~L+~oaIO4`&3}7m% z6V8v_{e7>N@iyI5&wjoRO4!y^%QCDAs&QzZm|(D~b-GE1Q7w8}79(9lf3R?03%%@0 z#`r%iKsQaFW?v*#iAc_D@>=_2ZvMUTyjm2aC6CS)B4?m%4k<2G>wS-aPs2{hB<+aw ztMn(shrc=q(ZspDrg`)U6aX+SzmeZB_)<4>eX9Z}wpNsF-F}8*(WhO(*B|~7NsI4r z0JO(9ZeCv9g`)~7>za1m+^~c_fN&Z!X&N_aO7RjW&o`~QzRL_KacrJ0H#O}1=uE#Z zGp^pQ@Zv||cEn5XFYUX~0e(bC(hx8mWEvqM*ezc@pFY}8jrVMR|HJ=U>Ebj|{c{dw z#XL-Z(UW-^o>N{x;nnVkNF?`?=d*%CEzQlp?=ZY9VXJZjI2cR2nmM$C+}X}}RS6J) zG+cye@YDjYt*&n-h*-lC;7@h|L3mWD+U>*&812CRD*R(A zg9<>~u{Pb%)sou0x5NefG0ce}wvk*ceJQ)v1ia!ELZAvUGU(&!x}E0+u~Qmu7Z0&bTGg|WW=(l^W*Du7Q#Y5Dh@8frvM+1{}` z&Zv2ploe#a8Ui%RKw z7c^6uqvvgowQ~2M8buWv2#siHXdnVb;i7-y1DXu*Uh-D*@&>pGHI!8v2FiI`C=|0= zr@jBYPg;u;QnHwHv}N9t$0U)&1USpS|;5rDfE zJi@-1>w`2bHTGOMXwE+nv;ucC0I?~HDCc_22%-4?H{-2sKs$Jz2tcoYL>FpbXtoLg zjG|zo8PLi^dcv(*B<%R7+3HW7H-6cup;I3P{5h1nyin4$P4RmVp z8Bn1~En?g%ui@RwRCmF`WM^QP6)Cc4g6NC-+PNm+J5|WM`S1mWvBGLTeGYP3;SmeL z`rrzd`XDvz8Q2F)YyVLWn>2RY2O#s#e#y#m0VWQkJtUz9t#PshnrKgCSN5RDIf5fE z`QY?e8>J|B{7N!aDIb1dOoTFBTDmiZAU}S(Tl8cY@0zt+(2UE|${^-EBmPNMR(;wW z0sK4&z8CQiF^c9_SQJApv~WHGU^43KIIH7Fy9fNR7r#b6w*k=L6rM>?UL1<$I^md* zH5jPBZ+rgzauEV3%s4Y>0%PfG953c*PoQq&fNUWk?P&rC4x=dH&1*$Is|hdpr-*W| zzQP>7R$w>Bcka%tzRY?}iBq%`_T30PJ9LCbC7I4?F#cAd7dh9g&y9S3CSVKeM_WkM zI^!~})we?qFR;;q)Gs~Q!iq_1#!P`3N9^N_;~66H>OuV)EZXy?)SU=d}dy5j0xb>(651vE!fGdu&BV;U z+eH%Et^u$>*cr9)V4$8p;iy(`6-AJI4R8MRI&j_WxCmH?e2HtL%84HSL4kCX+>raH zDJY2VSBmH3;t)7R)a4wH(b92|792uw;|< zVgmA2@uyL^LLFcdtL_3a>PqlfcbPO7@PA^-K*d{kG=x8xi9j~|)o z?|?E702Os`bpxTw9xn%WlSGAg z@>dFhVobnjtpw6T{hIrw{Q+kdvJI5Q&uKs=)mvu}WDgm#K~MqFg6pc2@$heT@Zp=6 zC8az7NPVXDQ-}raQx05wnbMUxRdtIdEV4A@&bh4-B!{0kbi9~n3XnZmK;9Gha;tJ6 z+Vn&$zT7@Op@;o89O{+(P`tsQV9?k2&W>H|=%&c(tq{_+!RkaLN0MQ_tmG&ANO^5u zzSNoLNsT?k3j)|VuDkBgysVrZ81FlO2r|WH0`3=;$ee>y%c`n}j$=e;SKA|&qvUBn zC8Oet#_LCQ;9Ch0)3sqiR(eUSIlgaW##E%_j7EUbYJ0by844k{u0H1&n{|pTxV2^e z6IqbJs_1`oHvB6>LwPexAmj<3@5XdEk^pkzd3N7LIL(iZn~3xzq>_j&5OQ-RB-I$V=Bo0sc6i%^Ig%?N@iz7idNzk&jQ{Yxbq5@Cio19 zEWw!383GZCXJNbiyA5SiX&FU<5{bD1h7XO%Fs0gFx;|D|z7hoLZX<28o&X zDb^fgkCPdBx8D$GgMl&vXxx2k!*Q2CW&ry%XcD5v-3K*xF}M=9u|J>zJG7R^)Au5B zhuyTF+z?KAGG`uXGP8(Qf?E?Fg^p=q77r>gsPgQIqzbV5mL&gD~MEMg3xEdV)ZkFKx75Cop zRKI`R_>qjtp|Uy1%FcF9Mk?8&gOI)Vp4l>zb!4w3ju~?7l|3_(ku6)c?9F|t&-eHE z{c-<)d-PAdul?Gu=j*zpk@=2~x^w)Sa|vt%BOPCLnSDt-U0q7|-lpoETgRRGtXk4k zr@yEH1(nXrd0(z^kkM3-q$*m@lievs>nQ&cr9l7@gXoZCdAer9PjiSo8b z=|k6Ljo}0*k8T#tKKkv7#y3}0%WhrvTcQBPT_6_Fb&=M6P*lX+;L6%%^^60&)!Iyj z%%%)b-i9&}w*mH+B|-r+a(E&~Qc za$GY$!3X7qUO6LY)psoxw}AA)?i5e{#!~o{p$K`?wlN`Vp(#SN2n+g?gTClZr)}$4 z#HWWa+}_CN&Vx`fA3P^mN+6IpkF<^Y4a}$`+5y4^Qwqj6o{0eP1%Lg~h)qnLQ0I_- zBuyj{UHzJ(>5Us>DS_nqj-{2u+gOHo_-r=@&P3ySy#7`f#S4;b+Mo0xzmV3U(s>PZaa=Uy}O!)NRUiz*2 zujy}v-{bpF$uRWZ*`Znn0DOrzfj%&Xpzy~66dNJ}Ob-~Gk03ISyl(pe;mk`t0o{ws zB5{?FH(`7A@|7I-^ySZyoTEB@_4K|X!?a~bp&PCN)>k5lq^Ga16r4y*A5TItISj9| zZ#3Y;$)>pK_df515su$C=vBf!ITUqM^yzpQb3>RO-#rOA0rNF*I%2nBu8i z*e5;9*dVDf2Lk+Ch^-k;oM=5U{6-&}{dDe?!9E(oDB%5Ev0*FcZ9b|WJw-QeO1$~F zRRYj2xWFW3kN}#g@8Eu6z()`j_Xg=Y^qw5k~V z9(fbYTBG-9;8MmpO-%VRDQwK_%QYSr(?zv_AF|GkwpG0^-o|BVe99lk=984(HwiPj zrO$VW2f!bTHJalQTyL1!1H;)AUukHJat$QhZa%_I;#)vo$Pt%}uvI*-=4uN6^!Dq% zlsV_>XVtrQ!l5u}R^&G#`IP2WirPE!p?7v~=6cZaQjB%txh|KwTz2<=U1=}O?z9bD zCC%aS$ie`Nj`GJ#XF+cH>TD-*2g6yXJ`h|gT))dl**Wm)m2lW!xXzMM2`NYy9SCpQ z0cS4P8D`BX_m>>LFmAX~-&i=HI-swlF2C@P)i4XBymv@hKQFcU;DmvCJH)5GpeCX6~bHgL7ZY2KSdc6Ps#IEO+(~B>r?vyAYtK75LUWX!>s1y#pvXg1wibXOlwB!3 zn`|KzYD$%$=J%meHnx+kg}!OxK8O?(yRSuf`%~)}AwS{DGUOR#+^0&12Dj3q&YbcP zZ6e__e68K@nzY}VCCm33v)Vq7=jeQ1g>_xkTv)pZ9yrA6zg0 zI`WocPg;l}wLf9xow4)_vbHgEzX9;3l_Bn4hhH}%gX6Yx~y67>A1hc0{$+*KVbb~7grc|&X+0S$Xy6*hn-8BSjPpIWuC{ zRBTre=X~@qT~>*y!vmmG%D}6Tbf5xZ3!g|hLpF(q*UO`) zG>jbN{*WymwL>(c$v1x-bcbOzC)Z2hkbGYB7jd$c)$4H#0y&w(mrn;HcsBD~-;lNm zPMj^vjzjsbu%rY}fpL=LrI8OXc!P4RicMG`x|Px_TuzUij;P{fejTCV+NvlahfP39 za61DQSArIO3upXXt-USMY3l3sb5g5l+VjpTJf^(y;S6&aIsc@!BiG%la3ZMmd|KQ1 zxs5&fGSq}E^BTzVto)6!$Mj52eyEj#jzYUo7D3xkpeq2s_RWL#`|EXp%m7xa922%l)H20LQrA z!akqD^)UVIIxhn(pbt~5bJR#rB*Bs(?xUh2grqz_Z-u2p;?MH>^`1IljBzC8N*uLil zJh4tcAmC!>2xL&O=(E(`Wg&E`2Y~7Bp81iT8%!y5!I?)`}(kQ;nBL`?_00IQ*zUE$Jaq2#yO>{ zS$E~h;kDTuaX1|ANKbEk7`64mQQ(OfyCQG89CMdd;M2H!e7GKAgyyIp9xdl?1@n)~ zq|kl2mUVABRdU}-yd1sS22eNSVJr6G>krIs{wa(A?>!qIh|Gm0+kQO{)L9ks{mq0= z35u1*I1r4qm@t|Y*JgtX`wioHH#s7J4Gw9UniHae)(1C`v%1AV8z3*cZ_vktJ=$)4 zSK9WS4G%|fT1z0r!~S08!|M#*Z%~*bIcqpiCO(eUFbuGA`oOc6;3YcN49cJ>{2&%k z*=SuaSn!ZHMVtrQ!w(0T3!n0@jLCUjj4=%;EEwZxExNOBtpxHcv!JxVuY;P7TRimk z)BDiFVee{3`sXT}j$$UN!#NnTkoS3+Q?_Va_vdr#Qg;UNWv`;edch3+^m4(hZSY~m zF-{HWaaqp&XV2ra-tU7dyD?&5 zE(N>Q;m*{40)Q94Ly&X57-KMoGekZTp0fsF1vJo@_W?9xa9YcG20NWDSe-WNBzFMT zf@j4f6ESIsu$f)N2_>W%OVf_?w5;EHyQC}O9U_-~{QLbpJ}#3j+jvu+k^{F!i7hLl zb8r4874O2&hA;q%?zA;%?d78Bh;6E82L-mY_4c$7TORK*DCK}Tny={1=N3bqm0WAM ze>Hya_L)9QzfWzt-%~Du(Fpj+t+4d^Gp0^dvm0U&3&sa2!_&FaLUtd^rvJK)Pls>d z;3YYR`nR*2136iH=YVpGF*YLZw7(SWp-$-0hQ1Mizgy%m78Rw(S5CaY&U4pg66~Nc zdOO0v{PL3*#iQHq5uey6KQv!_dpT*j(A)o!t+<^YQhwWra@Vwhk@G2yoN88v&;2!inuGlOMAGV}4<|!|F)HyHbRMnHg>Oocy0Z5{ z8la0;FD`tfR&NANAWO4y{f(t2*ynKb^o=-uwkEK z?8hjoYC9=g`JJNTVoQ<@@htDPp{K2VY@hzY9cACyX#_=Otl)l~>hwyN%U%@-=1^T( zULctU8mweZpNNpJJiOEV&rcx8D*4xg?yZq$2Xx)-zTv(PqM3JhD(B87dUqmCdnU|I zD&7W-5F6ZWa#;A){MBsuYyfLli3lVCXH54%5yG-@TAd-eh(~`y4Z$Ex(~;8KOz_ek z_#dx$jV{;OZFwCl?KoGzN^x7(M7h_3fBn0bMw0tmo}*Ci?`)4;ZzyBZ(I{^505==c$$G5~Q-fDy?{e7i7*wZc z!JNUako*lw?!!Ef#2mHI$pDr2cQobA#;xMOuMt_Id>x%)Ib(0j%=@h`<66e+zwTB36BV)P1ZN&I{y6}!*6Ck?8T?_ZwXgVenrD>E8I>9QV} zed{;B9Zim<7oU+#o&(^Tr@u$+!=m7lg>Z1^Rywi+I-($;vT+SWj%Siri990?g|5ml zUomRkB8C}{Q*76~|9b+`$Dz_q#S`GF0hnv(RXH()ygwRbcbt@sjJ>ee!t7!lP}q+T zk~ln?avq`xWt|AF!wvmUf6V>(n%A%|g<(n}3m|y$6IkARW!vA0o$SQkga}mZEaZ$? z8C8NnNRp70^K@ckH!DaQCf*q-yQBV;@0ft~^F2Wz43n;fR?e*gLk3sI?|NH$$`dBO z%*!9=Zm+#SVlo+gs(R*-q( z!&%slFcQq+Z+*vf>FarT$~fXO?RMt0Nx&>uAtM{#IgXOmL{cM)=kGfdOn;DvKhY|a zOwIN?@3BZDHHR^~j6dQvQ|k@bLmrVc@x1utI+)QJaN%cVwRI94oBhP+)0YhLn5mt1w~%m6WMTHq0hs zRCTlb#8#~P-joLYb(jW6tR2Kp;!a2C937#C(JO!V@m%ixaF6>U zqHPP}@kujk4f*$W@OL99yWmgjd{4;WK>|}_@@@wuFSWis^k+0o!F3|(WsA`dH`i#5 z(iHi26Ye}g=zAQ~oo4&`!P1{fi;3cA_z29SaZC8KV_ZK$N#|Nq;nmw%1#b$x=Dn1> z(I0tV!Mm&%sU>@m^&(u~bGw|ve{nBm!7iD(7vIOq-JhPLR?56MwLQtnx!i?Lk+0O} zI7W^0#;G?TdB0A%OWi6ND7ye{#0uu+lI}-t}UsdUqvZ&X9ubm{RvVQRzt;&JD zHM`ZtG653_Iq@~Ku%)mVua;+H{TSQL_WB6lrJd4-)8p^c6o|WEtMIh6v?91S*`^^F zdg1zEWNYzV;SARLC@lGAp9S<_g+O-~{QEu$o46>mo#ILocy1$8bRM6@`St!zg!#$E zxzhiz03LHlWf4NPMFOUiV7IkJfph)@hge$FgZ(h)mZ|n(I$|r9fmTav7cF&h#SL6V zTr4~_-ROMr)>>VN(6f#Nkx)duo)P|{BlP0fPw;)6!iKyUDK>&Vm93syviw=gITeYQ zb5fpHOS~cKs}$pnS|V9_E_O;MPA>j1_wq|4 z@4cDEuC>=r(Qb7RYZNLikZn%GR5n`RVuD%rZU?Wd7$EJbC1XtaLm?btz5*qk>(>3p)@YxvpA(_+nMWt z#}><>Gxw$-7-UTtq7xBM#w&(l?=gEMW+NZK*|eS@FVvjkbu7iz-3d?JnbJ&Z7p0VD zefLoAVQ^eQMuTU9uIzqY8S6CW(G*%H-?!;OQWx>kk>SlFlS-T zoC-2|Ss7-Tf)gm`cRw}7trBf4SiiT5gh7wCRzWF(P&`v%tiDmt1)?NnPWtn$|4xFx* zf%`EJQ5ctHQSZ=ar_|qBJZJNnl+rqWdpW98-yiQGut=gZI>0@a^>WY2&E&8?fhh#H@!!$4MY_0Wxu7nT$m|- z;};{<_=dEiv~=*`ByuDOB18-rgu$SK6|-5wU2S_JHN>YY0A7nxb!BbPsy6>RLBFQp zK-V3a8J)R~=Az{a6)ea!x7U0$Z;vJIBT#UEV6ms2hIY{kUC2-S@a^5bC)d-SB)OtoLc-o0mV4=DpN<5FzcuG9(|0Slv$XhK-$F zrPy`-m&SS)O63W5fl-fYfN0?OnO~Npzg>|OF};)Z1gB5 z8j@o}5%&jL+v%!c;(|XM@`9;aVfcU)U-IgbK*kAzC9G_;dYVGmS-|Ui`SWRh&e$5* zU%VvT7SQk#ZM2ul;Awk|*5c9VpT<~2CHI_OPo>PgvAQ{P4J+m`yNJ@A z2vu85?t#nx%KU?Ai12MNKzZGvj#-SKexH(KXeWK0oYIR_76%PDn5h-(ycnnT*~N2vQ>}(hR6>J8AI08H zxirjjJbp{}jsBaDTSoEA&$C7EZS_~chTsG`Me7{Dn#g~Aq5`?MP|scw)L>(VoMm+; zBBP0r^16jR9I&R;pI1QN_AL5Fd)ViAXVsf<_9s$WUwDmt7b5yvZj=3T8eNGH<1bWjd5rfrT*Ge=F>{J+K_K_T1C`^cLi^6dEZ_(-Fs^ zZoQAgO;+TIF^6?zQa5_oJGu6>)q7;8+oO|B5@&=jNg@`he+_B0jK39&GlXiLFsIed zJ}4*{*bs|}v(tFq%s?nxA}-S_*8hjeG%R_o)}~abXMd{Q#hQv8ff*XF56mKeFiZ|O zG3?3O|ed+uvJ+ByyW-|Tbv1<&Z+&63B8z zUES2Si=zM)vZGi-We}RI;wKox+;;xStK~P<{&vQGr;$`)ij^egtWn&M#Nvhzb8knO zrWX4&<_QiSruAxm6}p~+U*&7CAXC}6O)9?kI@YrejTMT&{7K(2E4WUXJ^MK`+HPp;L@uJs$UW(8rob47%|T^Iteg>tu8UDM5ChK% za9On3@F3lGtBw76;_*ggK-+^^TEhJV2YGY_)|I2$Ykzn6y8E1w|8Dp7u|3;&nlDQ2 z{`iVEw|_j14^3%=`lI#>R+nESK*4QU_&ko(IUI-c;Wa|e@`{R@TrY+7p-S*#C#nxI zUT<9!&X^JteC(2jzs93Mq9LBOoe{-bhw*9&FG?#CH#z(1q)cl76B@~7qFlt&u?n%{ zvAFRBP+zLcIoq)*bvQYV*7*<1@fR#J#``%o5XHo9MDwm8#|kb)|G{Rt#bGtOY_xOV z`eXE~0qBL{n|&2P5(;XqcgnKD^~`19eEhhSvlX1(JUV?F??fh%MMBf2yB8$t^$zK2 zK~%>cLb}eJ&yDp`{!}o;!$@HoP+J+tz2^OP=YV^ZkR`ryb;-Lfuw8~Jh>+TFnxovR zY9%w5rA;$Y1hk4K30T=fexyzpl2I0GjAp zex2GICJ9MYPjo1&V{}{WB1J`}_Fr+z0Q3;9P=i&F1xD&d{g7Ep#;XdlU2N)2dA}0$*CQb$L>Hm#WhtiJ4eE|Ab9h?-Z^piPCQ-c zH(wZkNktcA$}#zA5l_Pi9IR|~V(`h&ri=?$nZmU6XydZ=n_1Myk%}=}rN7&H@+vd4oIpV)8=iR{^(L z?`WE`@8~cJRk)WkdiGS=)klt}8_hb0*=7tl%6mhW$@~4@d}9XC1cI3&o-5yV1rQq< z5$5*eo2WtKNM~#aS@SvTqw^@b|t}zIw z<6B)tsJ6n+>1z9Z`y$L}U!NyA6&?D{NnLtpIU5|PcR(Qm^NBjGlgRD z{P|8?djfLx`X}$-pxX%u3*LeaHSW`1mkt=Dk{MV~5x>1i4Z$F6MSY@~>Uo-`pof{{ z599{uE<-Mkl-A3wqI>prY{Ds7Y-PY~oL^kCDq2>$(GAZK4d8(~wDSMfxbg!94V$k+!NqR?ypZ*x1-m8ks7;Ftujb`i7F4c6R$YUFvTGM*z0~xUpZuN6sc`&M%eMGN)0glX?^$p#C0W|7=O@biON&V-4=>{KZ_NEDJ(y zYGx+gm1Fm;X$4~-C@pDXGOXRg@taDAO}aKDQ?!|<{33Q}T=rL5G`r8<&b5v(SF||J zttY@_o$0#FXtI!vQ98f({9t`^bP5LoOU3AzbPao5EPJ%V_@$^XirPqR-`hf{frpaH zw@SHh+`OyeoO#YxAvkKjsg!3^(Zoc^@$*z=;7mw&;Wg3>-7Yly?27WLT2$Ei5sw7U zJazi8Ah2XB^(5vQdo|%DnP}s7bH-=0xR^J9hv`=G(hgIAr!+`n$@xj#FrMVNTKaf zGG7hiB#t@xqth3u7Sy*kvl=oHsNj~__2W+2vGMU8-=(oOhn^>xM$@kLu$pG5Z(yk9 zIop2Zuj0CX*sy(-RZQ=^`LdEMvz4I3Ia`<#97b+tEbSn6z85>yC<>T(J$mPuO*eV{ zdBwE(>uZo1e4yHG@UowAYujn7e{`<$GrAE&c*xkLy-!6OMBRA5d*?@Ir|}|H@P<-S zrF~00cQXmPO6wJrwwFVY>bsI`R*1mxAL3hJBkzqjB7u8Y(v6Gw$vKg44AJn6pp=%b znXOK{9?t~clWp}jd#Le+J_D*hUtMIelwR+#yO)EQI%jLc=2}YyKK=#6x>)h?x#-M~ zv0Q{s8@D3rvp5dI68P1wHaIOTM{^Vqq4;kTnK4Y?*vriD5u$0%tXp1`1h~Q{+$+IE zg0KuI#Ia59Ty!Y0PCSdmDR=B^wUp7U)Pyu?FI)5g_d4myTdu<#jkZW&W-?zH_SGY; z5I^GCxey*qV#1oP!^ndf4gFuU#1Vhol19u0cHZFk<3hMkKN~vloU`e;apW&PB9R61 ze^KJ)K!N1qYDS+D7An|%v-&r!=X$Zlgdeey!ft|>m6cxk8^N8k-9GubHGVb!{3V1v z>PLGAtaO1UI7AbcHWH}a_@wMj=R*7qscFfzQ&3u^-p`bI)e-v5 zO4vgx+9Uc5F>q#L!xVJRmhHtWxP@5^JNx2-lqAy_U8m2_P-v0M+b5PnAATIrVvHy( z5Qw6YC(fS`y2$J9yZGIAOXG>Ey?!3eQWiU8=Jk;!if75Z=ho6(nonmRKblFF-PiQ9 z5M&6*kQG%G-%um}6wMQr@tzVERi9FBc{n>Yd)RPIBIEfW+(l?0NrqTf`SYK%*ws?M zpAOa?#8_bh-L>uzzj>05?Sq4ga#v5!+K;v{$D=l6t}lm!ujxpVVIlJrTocj$f{Ys$3bF{&^AYUbB#WatmOfFzbmh)8DtF%>{dwCS(l~JQ zmM}>sZk$+$NbaxI2^xuNT}026uEK&rOK@cT#5rz@OZO4YdTJYVlrN66}*ER3CXj)n79} z%oFMk)mD`9n5g2MTh*1ubOe zz4nt1dxW#zKuZZqDEM-2`kZo-PS#LY(C?UO_LvR%BWW&%kVRBHYwrCyo9?`NQM)Y( z5WXeYh5@&kPQfMNH`VB{@Z!}wx>bG|WBbL;*E`onmIl)(iWX*N)u-2&)Ya9MmbMr} zA$Wu1X$%RrWr0*-pA##i^A@T06I*f-fE3MJmI8!;py@-D3>p-+$6t^X2mpyo3*NZX za4{IAXOoSdwXZ5Io%iv1H(%w+y29xm%sog!fO`R-6T$bZ9t}%4b2)JGHJ0mTV-Kdv z_S(((^Zg7PL8YZQ?rgVv&SvqWEQ9m(9Ub#jqa`rQcE-oXl7I_D`^?o9scaTXql{0i zEgi)7kJ@zH4s$?V3ZI2Kbza1hy(*Lv9Ko+qJd;?gT?)^`g+<@L+A}O*h8MkR{|*{J!c2;Eth@{O zycwfO_HtPD(x!%b_Xoo5>v-F8McX22T-Gl9G^W#{<?JB2D#maO5wjO-xVQq3pF}iND0^U(kT%C3nO@b5-mS}5FQ`QusIZl9GiFk2;B{P1 z6M_~HHHEG3TO?S)+kjX}f6DO_KJ-IpiDwnsnA{hNdB0w+pAT|?S7y4Yw7q1bYVo0}zp7Vh5>qkib$^G8eVA+zUS_x0myDxE$;Gar zY{SJ4OTPwz_!-Ucp%z-`>UXAn`?QzMs-;HL2Gq*aEw&i@z?rBzMpRfgk8h`?9Uf+o ze=%E;dhK-17D8qmUDp5k_OGQBR;vk*cKS%VsS zwmN%W5A~wlr9aHH#1@?TVn89w@*0zXrYt~OGRgLcc)aqJ)2P1_rxcvZpZVTD$Jsd< z7H;FSc?4YjOaXIE*+RrUGAm%+bmwwSU5$yF(7B&K^+Gqc|F%uyT+LnAo*hPYc_!o( z+P+HacZHD9<%}UpHTjBsgiMd8aX*A3P#pVIAV>m%%#4DNzw8s=(6EOYj_>;qil0xQ z@p7?{rw~-d2eh!C#5SHR9P@~RFLY57+`|HaD87hhsu?7)yGhQ)z9@`ZYK|BeB*54H`N<%vEndr&@{V5ech&>e^@h{C}MX?%*T1%&Gt170f}<{-B^3Q!%d zL9&bMzZB|;ejy;-2qDbBkl+p?I_UF-`+DeZ^A>Nb09V2rY-=0=uGcvzLThxOh>(g7 zO{dOg`)Gbkr00Wd`-h3r4;h}2;JD0-tOO?S<#ze=O5e)YYCd6fi$4X2Z*sAN=|Wya z@8^h4x)H-mZcx4YU?5lhb|?7hVl5tBBl61-#^CrZb%ue$JOqWAWAvEy*qhG*!2yOv zyjCB7ERY$(yC}kirB{%Vn%aZDW+<%B+v8&s>=5;=Ey3jXHS9Q>%ufxhEbFAS@Z?yn zg>I1B{BibmXwRY^&Q`W8#e$sZ6rsG~wm7!0Wcn4N-Rw6pqSYb;k z34Vmh(kqeI(k(J$sJCc?&LqlwNb@h}-}k+01F`;CU~Kdj+b~il>$H`HWXk%ziH2;b zPp0}S0|?$QDUj@nt_QrO=CfK#cg`))R$UzqS+VbSt3p8e1+A@yW)FcqGC_{Pi? zliC6%bxv&hyN=j8m|*z}MpYt_nwYSEYhWzs&Sj#B9>=wpDG8-xoNA;3(d_8YBUa!D zNpZmu7Oc0)UkKUQ&0_FcX)htD0rauQq61yf0ybj3-fzE!4Y2W$1DMT>{%Xiukj4Q) z?@`T|{hY)x!gtUx1esO`Qjyh^rg65K6Kw+t7C0Fpm}#U+PG-HzTYUuqK({5_0e1M-DKuw%*Mp(8erfA4-JbnaI{L-uhtE@S59*~rHvBsP7;6f z^5|rgn)$edfrS+EQc6Tf$$BL;UsJ3qFr%SJzuZ+4iOX&HWS zRt|m8Tq_9=jmzYZ(y9~^5tYm{1k2$D%Siy*vE4;e6*T7wtC-i%p(ReCyG>0vh2I>4 zYLCedUZQB^y+vm*glDfhtb^KKoOm?FdIHi|>>IZ<)Wgp0@L*&TQh+4vW%fCZENhUt zdpUvd8H$`+)@ z67@_%8Ay@50jy#dJlaNH7m2XdepVazU@-GFdkOaEtq{TjUiQDse5|aSH;ZdurAz8G zfQ@Lbjay(4y%d}zCY-@sah*=%9dXxJjsM%bl^twD=AZ4B&d;c`A2w_JfuKSyKinzM z%6sJ3;vk(Hj#E&~c%FvWQ)q*W_irK2CF<$-)9TYjznU-0@(wrvzB;j2>q6?l%`)bY zDI*@4fa;QsaPi`iunq<4E8(==37CdxhQ|^i!>mW)yZ_g|o!KkrG`&0Sqx> zZfZro54}h)@CRYkq8UsmT^&sPp}9NXmBIw=Al7Bp5b2^kt}Krc87kj8P%u6(?*2lv z6@rorL~>7>PZ-7dSST6zhQ{{7_9NIEPlV5WA6+%nFld9IJZfgeX#>Xa>|+@s&lH*Q zZrJ4s^#39LZ+YM=zuq_~Fw0ek)9#wWkS!2Ye&sLaOC}w{pC+e!iMA3VUlwW6Rp z6)&e2AdO<1fhgVv@=s>bS;LNJrkfYckE}>kcgPs;Q&sJAY{X>8h0^4Um6lETNKuN0Gk6M9jHkKmfFUdfxbL}D0!#a0e2xmI!RvQl6k2u;4 zihr6L#cQ`%uytYL;{JRg;%=@Ls(lBxOF7ay=jG?7`7i>6-{LczE2-tzOC}q|=90&X z&sI{q=ue0S+5SD>9n;Hm_^GdmJe-W*uLX~I)^O2~{nB0e+f}5YjHRbT^n9<3tMSIY zs||)=LaxHct)gCXbzXT?mO~#^UOn{6=r7c*O4sZ`s@iMF`rrnOPzrgP_v5v?&SYwQ zxCaD4R0SmrMtloQ0d2@cNgOvAAy@Y{xgYVc+}7C42c=T+-_&60dqrWY)A4nIyt1=n6RaVEiImDdwr z`37H&;9Dw#y0^yP6Rn%gj|xH}(qPN1QMUTBz&>!V4iC5hh8O$R^%vI(VISq;JuNw* zU(V%n?<|qr@8e@%E9VLu(s1oh4fzlvP+U@C@hi=c)XC?j5H3H$erFY=Ne+&8ZHTZy zPJDHmRixhKT4H1rp{SqHY)CuS0x4`o{nt=ZuEw@HARM6o*#7ceyt-EWlXfiFz$7+8 zz$}Az#5AW^n9(M0OvWZpXR*Aj>H$!K#BEAof-1mK1V5t)C!(3$2TO+>Pukb3rI@Qa zx71j|g)J~F);h9bEK*zRaqVH_=v1DYob=bgPWUkcD?gwDzY-rZnK!iLV6YiWPlED4hvnEeo-pU zsqampn^<^?x)ZFgQ6V@GYlNcib(D;dADWyq2rR*=RxTxv+H{D|7rkSCvRKDCg-@n4 z+7yViCILg>IC&o?`CjxjlLD^|j1$#4cy}BEff~p@d!hFL^3Y>U|M4mTWYqjYH-_%m-1U$@ZF3x|FBO#yM1o*>Q8r)6 zv^?`l+?PCxr8lfqn=9HyB~=iEBZF?YQpx_^Gl`ASAQB9zwk?Xt4gineO45*!loJU#3`cFseHrM3XQ4A%r0mEJ&e;m|jZ z|EG+ftO9ez?f$`Op6=Rr(j|S`ciIa`ue0%buV33AMO1HyP{q94e9honX3%W@(UyQV z%L_Xt$nVOnY0L)8SO(-)!*TM$Sw7m^!LVV1FMIA+=OnpobX0W%3u7Hu^v*zJC&@^( zuHuy9RI*G-<$aL2o}A*`I-2|Fk7@bJb;|kkTl%Jt5r-Vwu_j$LpaW3ltAE;{KwP}Y z&HlGV7P!>BQq*bWFn^^zsdJJ#iC@ZYP4@Uq*u$V;T zqCf`G)N8h}i9xHjP0QBR2`3}3C`$zL%yB=Yv4(L5^bEm#Up36cm@w@cJhD{we2WAfxL*Yg~#EUwCAtSkS$ipXeikvl9WcUJwf6%wnt z<{vp^dZV-Xekg>)NK+Ue#uy6fJ0bR>Kw1TxZFwI|x;xdYa}DAPCYZ?xulFef67pDe z-Xh;beH7LqSbu$QpgRwMPJY|vH28{8Zy?o^cs^_XF`#UG^Bl%iy}C6BX5-35MMT3#%*u2HMZ?lq||zxvVcdMK8~))WDTg zs93^Jwlgk-im$^)AOEMi5*)CFpR0r-^VZEnc@*X)b^~dG@G_RS2R$A>zg+JvaGk%9 z0o~9|4xc0UUQjrZJPEI6VOJ|jiA&@OeePUU!mj`1DwYKCV+2}QapI14a2BlMAeZow zAER*tnW`m8esm_7LH}Ss+O5W!(Q`RY;JjtxAm#Kz$z{L1QrCNe=<3qm)hpNj2&=mx;_h|1&?OIq}DYqJ)y?{@wrV-&*<4YPf zOWFbh=9LT9lQ9uA(PUC;uA^S2x(a~M%~RX;r7b+7A(kpm&zU@4jBPb=ec-Xw z^1;)uyYp9?YdB@~?;EK0i}|5PwSSos3NAE%{jJ(!DP0ql56=<63_Jt3#@g!>|L<$r zXk*IYQ>@PGHf$<*_OHw1tdoDg$p^7|S<}(g6P1Fw4&a}s|Hs7DAm3M`x}Klc#|@0{ z{$AYUma_8$)6XTtH_)lKNa;MfyW0Z$rEegvT3|?AU0tb0)= zD+(Krc0@h+6 zxEq@Zf?F8oj}BUWMCf`Rmb0`CJ$leJUehY#!p?@zwoVmVZm%Qm1;4cNEjU)->X}5HNhMwzC(+P<4hJ&VC}!eNR2O=q^`U zP|DW45^s5J{=`0=Q2CMv}8<_r+L7}VTZj>z{=MHk~-Je?WcA_E-j*ez>cuC~y zX07)e9jv%=ky~xCXpfCZW(jxAr2hFfcE;#+)OdzE&ZzZ2;S{e9n2h6A58_PqGMVy~ zmkpb3b8emBw+n0KsD23bzd1noBzv}F+Zin(V=rk8FJi%>b`y09XaSt_*K*cbD%$lS0x79Z?mC93cgWMakd_*8i>6VCB z`%CE8WeZ+QZH~`L_K<)I|Ivu#p%VXj zK_@nPnU|vcn*jA;ibft-Yx|nzY;P+_KKcJU)Dn?AZS;3$nrL=Gn$LkWmC8Eimz=hN z()~V$8-*cy#oRg3dv(LSbl}ll9*P(11gigx>BhsNY~K*It)J5vk>l zL&pF6d%9{fp)Bw{Pd274K((rxFt#I9-}T>NV0a!xzMCJdz=Lgja>F@sxmcV(LGT`5 zh36oU4ogo@&+&K@?c4!{ruQe#HL`1Gx&yUwx-fxTK!; zu>uEy^>1m<4rhG_gVQ(<$7h8LSD4+7HizBi>%x4oqnjL zDYw5HFvvCWcjm0={@C*A$+j}l7bAZzhspo`@(L66joNEH0l2__Cvyw9amnYM(mDVB z@Avm~A&`Pc|E3SY?EKShkjAd7ru-vrT68BpITN%XU=Jh{_?`06apB=la^HEC%Zz&b ztKUAR0i8B~-%O~-?9%Nmqc=^zYyq^$)sN}Ol|Mmvir$hPF#EX2krsaWeC^+#Kz{D; zClHRv|4l;l|2B#L#~*#YP_N&Kv+x8I7s34|^{d~=N-944@>Jjd{{Sl>DqR2o literal 0 HcmV?d00001 From 788f2375f7a73364eba4582b36cee9be6e6eddb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Wed, 15 Apr 2026 12:49:44 +0200 Subject: [PATCH 067/171] Fix: additional information in docstring; Add: catch dimension error --- splinepy/helpme/create.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 726d5a5de..a57ef420f 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -324,23 +324,27 @@ def swept( Parameters ---------- cross_section : Spline - Cross-section to be swept + Cross-section to be swept. Requires parametric dimension 1 or 2 trajectory : Spline - Trajectory along which the cross-section is swept + Trajectory along which the cross-section is swept. + Requires parametric dimension 1 cross_section_normal : np.array Normal vector of the cross-section Default is [0, 0, 1] anchor : str - Strategy used to determine which point of the cross-section is - placed on the trajectory. Supported values are: - ``"auto"``, ``"parametric"``, ``"control_box"``, - and ``"geometry_box"``. + Defines which reference point of the cross-section is placed on the + trajectory during sweeping. + ``"parametric"`` uses the point evaluated at the center of the + parametric bounds. + ``"control_box"`` uses the center of the control-point bounding box. + ``"geometry_box"`` uses the center of the bounding box of sampled + geometric points on the cross-section. ``"auto"`` uses ``"geometry_box"`` for curve cross-sections and ``"parametric"`` for surface cross-sections. set_on_trajectory : bool If True, the cross-section will be placed at the evaluation - points of the trajectory's knots. If False, the cross-section - will be placed at the control points of the trajectory. + points of the trajectory. If False, the cross-section will be + placed at the control points of the trajectory. Default is False. rotation_adaption : float Angle in radians by which the cross-section is rotated around @@ -359,6 +363,10 @@ def swept( ------- swept_spline : Spline Spline resulting from the sweep + + Examples + -------- + See ``examples/show_swept.py`` for example usages of ``swept()``. """ from splinepy import NURBS as _NURBS @@ -382,6 +390,11 @@ def swept( ) if not trajectory.para_dim == 1: raise ValueError("trajectory must have a parametric dimension of 1") + if cross_section.para_dim > 2: + raise ValueError( + "cross_section must have a parametric dimension of at most 2" + ) + if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") if not isinstance(anchor, str): From e4fed5015b65651e10ef48b3c36947db260029d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Wed, 15 Apr 2026 13:12:33 +0200 Subject: [PATCH 068/171] Add: cross-section normal gets more rigorous checks --- splinepy/helpme/create.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index a57ef420f..c55ebc314 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -400,15 +400,16 @@ def swept( if not isinstance(anchor, str): raise TypeError("anchor must be a string") - if cross_section_normal is not None and not len(cross_section_normal) == 3: - raise ValueError( - "cross_section_normal must be a 3D vector (array of length 3)" - ) - # setting default value for cross_section_normal if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) # add debug message _log.debug("No cross_section_normal given. Defaulting to [0, 0, 1].") + else: + cross_section_normal = _np.asarray(cross_section_normal).ravel() + if cross_section_normal.shape != (3,): + raise ValueError( + "cross_section_normal must be array-like and a 3D vector" + ) anchor = anchor.lower() if anchor == "auto": From 2ef0bc1c8009ee67b560f4e2c2bad81a200bbb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Wed, 15 Apr 2026 14:50:15 +0200 Subject: [PATCH 069/171] Fix: more informative log messages --- splinepy/helpme/create.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index c55ebc314..6a416eaf4 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -457,7 +457,8 @@ def swept( B.append(temp_cross / _np.linalg.norm(temp_cross)) # add debug message _log.debug( - "Division by zero occurred. Using alternative vector for B." + "The initial trajectory direction led to an ambiguous sweep " + "orientation. Using an alternative internal reference direction." ) # initialize transformation matrices and tangent-vector-collection @@ -477,13 +478,14 @@ def swept( e1_norm = _np.linalg.norm(e1) else: raise ValueError( - "Cannot determine sweep direction because the first " + "Cannot determine sweep direction because the " "trajectory tangent is too small." ) # add debug message _log.debug( - f"Division by zero occurred. Applying an approximation " - f"by using the previous tangent e1 for parametric value {par_value[i]}" + "The trajectory tangent is too small at parametric value " + f"{par_value[i]}. Reusing the previous tangent direction " + "to continue the sweep frame construction." ) e1 = (e1 / e1_norm).ravel() # collecting tangent vectors for later use @@ -493,8 +495,9 @@ def swept( B.append(B[i] - _np.dot(B[i], e1) * e1) if _np.linalg.norm(B[i + 1]) < _settings.TOLERANCE: _log.warning( - f"Vector B[{i + 1}] is close to zero. Adjusting " - f"to avoid division by zero." + "The automatically constructed sweep orientation became " + f"degenerate at parametric value {par_value[i]}. Applying a " + "small numerical adjustment to continue." ) B[i + 1] += _settings.TOLERANCE B[i + 1] /= _np.linalg.norm(B[i + 1]) @@ -540,8 +543,10 @@ def swept( ) if _np.linalg.norm(B_rec[i + 1]) < _settings.TOLERANCE: _log.warning( - f"Vector B_rec[{i + 1}] is close to zero. Adjusting " - f"to avoid division by zero." + "The corrected sweep orientation for the closed " + f"trajectory became degenerate at parametric value " + f"{par_value[i]}. Applying a small numerical adjustment " + "to continue." ) B_rec[i + 1] += _settings.TOLERANCE B_rec[i + 1] /= _np.linalg.norm(B_rec[i + 1]) @@ -562,8 +567,9 @@ def swept( # check if the beginning and the end of the B-vector are the same if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3): _log.warning( - "Vector calculation is not exact due to the trajectory being closed" - " with non-matching tangent vectors at start and end points." + "The sweep orientation could only be matched approximately " + "because the closed trajectory has non-matching tangent " + "directions at its start and end." ) ### ROTATION MATRIX ### From 5a67a472ad6d322c210976302ced4770549c25d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Wed, 15 Apr 2026 15:01:55 +0200 Subject: [PATCH 070/171] Fix: no more redundant check of first derivative --- splinepy/helpme/create.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 6a416eaf4..94b6770e0 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -464,32 +464,27 @@ def swept( # initialize transformation matrices and tangent-vector-collection T = [] A = [] - tang_collection = [] + tang_collection = [e1] # evaluating transformation matrices for each trajectory point for i in range(len(par_value)): # calculation according to NURBS Book, eq. 10.27 # tangent vector e1 on trajectory at parameter value i - e1 = trajectory.derivative([par_value[i]], [1]) - e1_norm = _np.linalg.norm(e1) - if e1_norm < _settings.TOLERANCE: - if tang_collection: + if i > 0: + e1 = trajectory.derivative([par_value[i]], [1]) + e1_norm = _np.linalg.norm(e1) + if e1_norm < _settings.TOLERANCE: e1 = tang_collection[-1] e1_norm = _np.linalg.norm(e1) - else: - raise ValueError( - "Cannot determine sweep direction because the " - "trajectory tangent is too small." + # add debug message + _log.debug( + "The trajectory tangent is too small at parametric value " + f"{par_value[i]}. Reusing the previous tangent direction " + "to continue the sweep frame construction." ) - # add debug message - _log.debug( - "The trajectory tangent is too small at parametric value " - f"{par_value[i]}. Reusing the previous tangent direction " - "to continue the sweep frame construction." - ) - e1 = (e1 / e1_norm).ravel() - # collecting tangent vectors for later use - tang_collection.append(e1) + e1 = (e1 / e1_norm).ravel() + # collecting tangent vectors for later use + tang_collection.append(e1) # projecting B_(i) onto the plane normal to e1 B.append(B[i] - _np.dot(B[i], e1) * e1) From 80538f1bb863611954d237b9553a2130d8cc2029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Wed, 15 Apr 2026 15:26:23 +0200 Subject: [PATCH 071/171] Fix: moved parameter validiation --- splinepy/helpme/create.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 94b6770e0..654f383d8 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -390,6 +390,7 @@ def swept( ) if not trajectory.para_dim == 1: raise ValueError("trajectory must have a parametric dimension of 1") + if cross_section.para_dim > 2: raise ValueError( "cross_section must have a parametric dimension of at most 2" @@ -397,20 +398,33 @@ def swept( if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") - if not isinstance(anchor, str): - raise TypeError("anchor must be a string") + + if rotation_adaption is not None: + try: + rotation_adaption = float(rotation_adaption) + except TypeError: + raise TypeError( + "rotation_adaption must be a number (float, int) or None" + ) if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) # add debug message _log.debug("No cross_section_normal given. Defaulting to [0, 0, 1].") else: - cross_section_normal = _np.asarray(cross_section_normal).ravel() + try: + cross_section_normal = _np.asarray(cross_section_normal).ravel() + except (TypeError, ValueError): + raise TypeError( + "cross_section_normal must be array-like and a 3D vector" + ) if cross_section_normal.shape != (3,): raise ValueError( "cross_section_normal must be array-like and a 3D vector" ) + if not isinstance(anchor, str): + raise TypeError("anchor must be a string") anchor = anchor.lower() if anchor == "auto": anchor = ( @@ -596,13 +610,6 @@ def swept( # rotate cross-section around trajectory tangent vector (e1) if wanted if rotation_adaption is not None: - try: - rotation_adaption = float(rotation_adaption) - except TypeError: - raise TypeError( - "rotation_adaption must be a number (float, int) or None" - ) - R = _np.matmul( _arr.rotation_matrix_around_axis( axis=[1, 0, 0], rotation=rotation_adaption, degree=False @@ -625,7 +632,7 @@ def swept( cs_min = cross_section.control_points.min(axis=0) cs_max = cross_section.control_points.max(axis=0) cs_center = 0.5 * (cs_min + cs_max) - else: + else: # geometry_box if cross_section.para_dim == 1: sample_resolution = max( 101, 4 * cross_section.control_points.shape[0] From 5fc89a334ad02fe845f0b88a8909a37df23c6b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Obermair?= Date: Wed, 15 Apr 2026 15:40:25 +0200 Subject: [PATCH 072/171] Add: information about sweeping method in docstring --- splinepy/helpme/create.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index 654f383d8..a9d4cc4f4 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -318,8 +318,15 @@ def swept( The sweeping process has some limitations, since the cross-section cannot be preserved exactly along the whole trajectory. - The maths behind can be found in the NURBS Book, Piegl & Tiller, - 2nd edition, chapter 10.4 Swept Surfaces. + This implementation follows the skinning-based swept surface + construction described in The NURBS Book, Piegl & Tiller, 2nd + edition, chapter 10.4, where cross-section instances are placed + along the trajectory and skinned afterwards. + + The cross-section orientation is determined using the projection + normal method of Siltanen and Woodward, as described in chapter + 10.4, Eq. (10.27). For closed trajectories, the orientation is + corrected as described on p. 483. Parameters ---------- From 28a6a0083835f2b5f5406d427efa09203e3c8e1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:21:12 +0000 Subject: [PATCH 073/171] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.7 → v0.15.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.7...v0.15.10) - [github.com/pre-commit/mirrors-clang-format: v22.1.1 → v22.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.1...v22.1.3) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 388115dcd..d4d60e34a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,13 +35,13 @@ repos: additional_dependencies: [tomli] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.7 + rev: v0.15.10 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v22.1.1 + rev: v22.1.3 hooks: - id: clang-format types_or: [c, c++] From 63c49abfe56186e4178ae44417eeac4bfc4cdf36 Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Tue, 14 Apr 2026 15:11:01 +0200 Subject: [PATCH 074/171] Fix: Add py314, zip(required=true) for most --- .github/workflows/main.yml | 8 +- .github/workflows/test_full.yml | 2 +- .pre-commit-config.yaml | 1 + docs/source/handle_markdown.py | 3 +- examples/show_readme.py | 2 +- pyproject.toml | 4 +- splinepy/bspline.py | 8 +- splinepy/helpme/check.py | 10 +-- splinepy/helpme/create.py | 12 +-- splinepy/io/gismo.py | 27 ++++--- splinepy/io/irit.py | 4 +- splinepy/io/mfem.py | 4 +- splinepy/io/svg.py | 61 +++++++-------- splinepy/microstructure/microstructure.py | 19 +++-- splinepy/microstructure/tiles/__init__.py | 4 +- splinepy/utils/data.py | 8 +- tests/conftest.py | 13 ++-- tests/data/mfem_cartesian_2d.mesh | 1 + tests/data/mfem_cartesian_3d.mesh | 1 + tests/helpme/test_create.py | 6 +- tests/io/test_cats.py | 2 + tests/io/test_gismo.py | 92 +++++++++++++++-------- tests/io/test_iges.py | 2 +- tests/io/test_irit.py | 4 +- tests/io/test_json.py | 8 +- tests/io/test_mfem_export.py | 18 +++-- tests/io/test_npz.py | 4 +- tests/test_bezier_extraction.py | 2 +- tests/test_bezier_operations.py | 4 +- tests/test_knot_vectors.py | 7 +- tests/test_kv_manipulation.py | 2 +- tests/test_multipatch.py | 4 +- tests/test_normalize_knot_vectors.py | 8 +- 33 files changed, 206 insertions(+), 149 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4dda091cc..d1e309bd2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: arch: [x86_64] - cw_build: ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"] + cw_build: ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] steps: - uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: strategy: matrix: arch: [arm64] - cw_build: ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"] + cw_build: ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] steps: - uses: actions/checkout@v4 @@ -60,7 +60,7 @@ jobs: strategy: matrix: arch: [x86_64] - cw_build: ["cp39*many*", "cp310*many*", "cp311*many*", "cp312*many*", "cp313*many*"] + cw_build: ["cp310*many*", "cp311*many*", "cp312*many*", "cp313*many*", "cp314*many*"] steps: - uses: actions/checkout@v4 @@ -85,7 +85,7 @@ jobs: strategy: matrix: arch: [AMD64] - cw_build: ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"] + cw_build: ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml index 21b502252..9f060ac53 100644 --- a/.github/workflows/test_full.yml +++ b/.github/workflows/test_full.yml @@ -18,7 +18,7 @@ jobs: SPLINEPY_GITHUB_ACTIONS_BUILD: True strategy: matrix: - python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, macos-latest, windows-latest] steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4d60e34a..d2ccf8deb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,7 @@ repos: - id: check-yaml - id: debug-statements - id: end-of-file-fixer + exclude: '^tests/data/.*$' - id: mixed-line-ending - id: requirements-txt-fixer - id: trailing-whitespace diff --git a/docs/source/handle_markdown.py b/docs/source/handle_markdown.py index 8bb227723..c76566de2 100644 --- a/docs/source/handle_markdown.py +++ b/docs/source/handle_markdown.py @@ -16,7 +16,6 @@ import pathlib import re import warnings -from typing import List, Tuple # Path to this file. file_path = os.path.abspath(os.path.dirname(__file__)) @@ -38,7 +37,7 @@ def get_markdown_links(line: str) -> str: return possible or "" -def get_special_links(line: str) -> List[Tuple[str, str]]: +def get_special_links(line: str) -> list[tuple[str, str]]: """Get the special links from a string. Args: diff --git a/examples/show_readme.py b/examples/show_readme.py index d7456e08a..ead7f06f0 100644 --- a/examples/show_readme.py +++ b/examples/show_readme.py @@ -97,7 +97,7 @@ para_queries = [] phys_queries = [] colors = ["red", "yellow", "blue"] -for q, pc, c in zip(queries, physical_coordinates, colors): +for q, pc, c in zip(queries, physical_coordinates, colors, strict=False): para_q, phys_q = gus.Vertices([q]), gus.Vertices([pc]) para_q.show_options["c"] = c para_q.show_options["r"] = 10 diff --git a/pyproject.toml b/pyproject.toml index c071f106b..a984dae74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,11 @@ classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Natural Language :: English", "Topic :: Scientific/Engineering", ] @@ -91,7 +91,7 @@ lint.select = [ "A", # flake8-builtins ] lint.fixable = ["ALL"] -target-version = "py38" +target-version = "py312" lint.ignore = [ "PLR2004", # TODO! "PLR0912", # Too many branches diff --git a/splinepy/bspline.py b/splinepy/bspline.py index b932b27df..237e3c255 100644 --- a/splinepy/bspline.py +++ b/splinepy/bspline.py @@ -108,8 +108,7 @@ def insert_knots(self, parametric_dimension, knots): if min(knots) < min(self.knot_vectors[parametric_dimension]): raise ValueError( - "One of the query knots not in valid knot range. " - "(Too small)" + "One of the query knots not in valid knot range. (Too small)" ) inserted = _splinepy_core.insert_knots( @@ -171,7 +170,7 @@ def determine_new_knots(kv_unique, n_knots): return new_knots # determine new knots for each para_dim and insert the knots - for para_dim, n_k in zip(para_dims, n_knots): + for para_dim, n_k in zip(para_dims, n_knots, strict=True): new_knots = determine_new_knots( # recompute unique to allow duplicating para_dims. kv_unique=self.unique_knots[para_dim], @@ -303,8 +302,7 @@ def remove_knots(self, parametric_dimension, knots, tolerance=None): if min(knots) < min(self.knot_vectors[parametric_dimension]): raise ValueError( - "One of the query knots not in valid knot range. " - "(Too small)" + "One of the query knots not in valid knot range. (Too small)" ) removed = _splinepy_core.remove_knots( diff --git a/splinepy/helpme/check.py b/splinepy/helpme/check.py index 86f975bd0..4b6013d43 100644 --- a/splinepy/helpme/check.py +++ b/splinepy/helpme/check.py @@ -38,9 +38,9 @@ def valid_queries(spline, queries): error_query = _np.argmin(queries, axis=0)[error_dim] raise ValueError( f"Query request out of bounds in parametric dimension " - f"{error_dim}. Detected query {queries[error_query,:]} at " + f"{error_dim}. Detected query {queries[error_query, :]} at " f"positions {error_query}, which is out of bounds with " - f"minimum values {bounds[0,:]}." + f"minimum values {bounds[0, :]}." ) # Check maximum value @@ -50,9 +50,9 @@ def valid_queries(spline, queries): error_query = _np.argmax(queries, axis=0)[error_dim] raise ValueError( f"Query request out of bounds in parametric dimension " - f"{error_dim}. Detected query {queries[error_query,:]} at " + f"{error_dim}. Detected query {queries[error_query, :]} at " f"positions {error_query}, which is out of bounds with " - f"maximum values {bounds[1,:]}." + f"maximum values {bounds[1, :]}." ) return True @@ -78,7 +78,7 @@ def clamped_knot_vectors(spline, warning=True): if degrees is None or knot_vectors is None: return None - for d, kv in zip(degrees, knot_vectors): + for d, kv in zip(degrees, knot_vectors, strict=True): kv_arr = _np.asanyarray(kv) front = all(abs(kv_arr[: (d + 1)] - kv_arr[0]) < _settings.TOLERANCE) diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index a9d4cc4f4..fa407caaa 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -106,8 +106,7 @@ def extruded(spline, extrusion_vector=None): ) else: raise ValueError( - "Dimension Mismatch between extrusion extrusion vector " - "and spline." + "Dimension Mismatch between extrusion extrusion vector and spline." ) # Start Extrusion @@ -222,7 +221,7 @@ def revolved( # rotation-matrix is only implemented for 2D and 3D problems if cps.shape[1] not in {2, 3}: raise NotImplementedError( - "Sorry," "revolutions only implemented for 2D and 3D splines" + "Sorry,revolutions only implemented for 2D and 3D splines" ) # Angle must be (0, pi) non including @@ -826,6 +825,7 @@ def determinant_spline(spline): spline.unique_knots, spline.knot_multiplicities, multiplicity_increase, + strict=True, ): # increase knot multiplicities: # @ each inner knot -> mult_inc + 1 @@ -840,7 +840,9 @@ def determinant_spline(spline): [ len(kvs_ds) - d_ds - 1 for kvs_ds, d_ds in zip( - knot_vectors_determinant_spline, degrees_determinant_spline + knot_vectors_determinant_spline, + degrees_determinant_spline, + strict=True, ) ] ) @@ -926,7 +928,7 @@ def parametric_view(spline, axes=True, conform=False): # process knots to insert if spline.has_knot_vectors: for i, (kv, d) in enumerate( - zip(spline.knot_vectors, spline.degrees) + zip(spline.knot_vectors, spline.degrees, strict=True) ): n_repeating = int(d + 1) query = kv[n_repeating:-n_repeating] diff --git a/splinepy/io/gismo.py b/splinepy/io/gismo.py index ec66f5662..9b1405540 100644 --- a/splinepy/io/gismo.py +++ b/splinepy/io/gismo.py @@ -270,7 +270,7 @@ def add_assembly_options( children_list = [] for label, description, value, number_type in zip( - labels, descriptions, values, number_types + labels, descriptions, values, number_types, strict=True ): children_list.append( { @@ -644,7 +644,9 @@ def export( boundary_data.text = "\n".join( [ str(patch_id + index_offset) + " " + str(local_face_id + 1) - for (patch_id, local_face_id) in zip(*face_id_list) + for (patch_id, local_face_id) in zip( + *face_id_list, strict=True + ) ] ) else: @@ -657,7 +659,9 @@ def export( boundary_data.text = "\n".join( [ str(sid + index_offset) + " " + str(bid + 1) - for (sid, bid) in zip(boundary_spline, boundary_face) + for (sid, bid) in zip( + boundary_spline, boundary_face, strict=True + ) ] ) ### @@ -685,7 +689,9 @@ def export( bc.text = "\n".join( [ str(sid) + " " + str(bid + 1) - for (sid, bid) in zip(bc_data_i[0], bc_data_i[1]) + for (sid, bid) in zip( + bc_data_i[0], bc_data_i[1], strict=True + ) ] ) @@ -704,9 +710,9 @@ def export( as_base64=as_base64, field_mask=field_mask, ) - field_xml.find("MultiPatch").find("patches").text = ( - f"{index_offset} " f"{n_patches - 1 + index_offset}" - ) + field_xml.find("MultiPatch").find( + "patches" + ).text = f"{index_offset} {n_patches - 1 + index_offset}" if int(_python_version.split(".")[1]) >= 9 and indent: _ET.indent(field_xml) file_content = _ET.tostring(field_xml) @@ -723,7 +729,7 @@ def export( if n_patches != len(multipatch.patches): raise RuntimeError("Help - some patches were not recognised") - patch_range.text = f"{index_offset} " f"{n_patches - 1 + index_offset}" + patch_range.text = f"{index_offset} {n_patches - 1 + index_offset}" # Add additional options to the xml file if additional_blocks is not None: @@ -976,12 +982,11 @@ def make_dictionary(ETelement): list_of_options.append(make_dictionary(child)) else: _debug( - f"Found unsupported keyword {child.tag}, which will be" - " ignored" + f"Found unsupported keyword {child.tag}, which will be ignored" ) continue - _debug(f"Found a total of {len(list_of_splines)} " f"BSplines and NURBS") + _debug(f"Found a total of {len(list_of_splines)} BSplines and NURBS") multipatch = _Multipatch(list_of_splines) if interface_array is not None: if invalid_integer in interface_array: diff --git a/splinepy/io/irit.py b/splinepy/io/irit.py index b00dca1d7..6fe88983e 100644 --- a/splinepy/io/irit.py +++ b/splinepy/io/irit.py @@ -62,7 +62,9 @@ def extract_relevant_text(lines): spline_list = [] # Loop over text file data - for type, data in zip(spline_strings[1::2], spline_strings[2::2]): + for type, data in zip( + spline_strings[1::2], spline_strings[2::2], strict=True + ): # Dictionary of current spline spline = {} diff --git a/splinepy/io/mfem.py b/splinepy/io/mfem.py index 750c9b951..f2d2cdec3 100644 --- a/splinepy/io/mfem.py +++ b/splinepy/io/mfem.py @@ -448,6 +448,7 @@ def _corner_vertex_ids(spline): for row, boundary_id in zip( boundaries.reshape(-1, n_vertex_per_boundary).tolist(), boundary_ids.tolist(), + strict=True, ) ) ) @@ -456,7 +457,7 @@ def _corner_vertex_ids(spline): f.write(f"\n\nedges\n{0}\n") # Write Number Of vertices - f.write(f"\nvertices\n{int(_np.max(vertex_ids)+1)}\n\n") + f.write(f"\nvertices\n{int(_np.max(vertex_ids) + 1)}\n\n") # Export Splines f.write("patches\n\n") @@ -497,6 +498,7 @@ def _corner_vertex_ids(spline): for (coords, weight) in zip( spline.control_points.tolist(), spline.weights.tolist(), + strict=True, ) ) + "\n\n" diff --git a/splinepy/io/svg.py b/splinepy/io/svg.py index b8fd6e40b..93d704f2a 100644 --- a/splinepy/io/svg.py +++ b/splinepy/io/svg.py @@ -271,7 +271,7 @@ def _export_gustaf_object( cmap_style = gus_object.show_options.get("cmap", "jet") colors = [ _rgb_2_hex(r, g, b) - for r, g, b, in _color_map( + for r, g, b in _color_map( values.ravel(), name=cmap_style, vmin=v_min, vmax=v_max ) ] @@ -284,7 +284,7 @@ def _export_gustaf_object( id="control_points", ) - for vertex, color in zip(gus_object.vertices, colors): + for vertex, color in zip(gus_object.vertices, colors, strict=True): _ET.SubElement( svg_control_points, "circle", @@ -320,7 +320,7 @@ def _export_gustaf_object( svg_labels.attrib["stroke"] = "none" dx = radius - for ctp, label in zip(gus_object.vertices, labels): + for ctp, label in zip(gus_object.vertices, labels, strict=True): text_element = _ET.SubElement( svg_labels, "text", @@ -395,7 +395,7 @@ def _export_control_mesh( "g", id="mesh", style=( - f"fill:none;stroke:{_rgb_2_hex(r,g,b)};stroke-opacity:{a};" + f"fill:none;stroke:{_rgb_2_hex(r, g, b)};stroke-opacity:{a};" f"stroke-width:{stroke_width};stroke-linecap:round" ), ) @@ -442,7 +442,6 @@ def _export_control_mesh( # Then control points if control_point_requested: - # Relevant options: # - control_point_ids # - control_point_alpha @@ -735,7 +734,7 @@ def _add_scalar_bar(svg_element, box_size, **kwargs): "g", id="tick_mark_lines", style=( - f"fill:none;stroke:{_rgb_2_hex(r,g,b)};stroke-opacity:{a};" + f"fill:none;stroke:{_rgb_2_hex(r, g, b)};stroke-opacity:{a};" f"stroke-width:{stroke_width};stroke-linecap:round" ), ) @@ -775,7 +774,7 @@ def _add_scalar_bar(svg_element, box_size, **kwargs): ), ) - for mark, position in zip(tick_marks, tick_mark_positions): + for mark, position in zip(tick_marks, tick_mark_positions, strict=True): # Draw a line _ET.SubElement( svg_tick_marks, @@ -987,7 +986,6 @@ def _approximate_curve(original_spline, tolerance): # Check if a field is to be plotted if spline.show_options.get("data", None) is not None: - # spline.show() _export_spline_field( spline, svg_spline, box_min_x, box_max_y, **kwargs @@ -1008,15 +1006,15 @@ def _approximate_curve(original_spline, tolerance): spline_copy = _approximate_curve(spline, tolerance) bezier_elements = spline_copy.extract.beziers() path_d = ( - f"M {bezier_elements[0].cps[0,0]-box_min_x}," - f"{box_max_y - bezier_elements[0].cps[0,1]}" + f"M {bezier_elements[0].cps[0, 0] - box_min_x}," + f"{box_max_y - bezier_elements[0].cps[0, 1]}" ) path_d += " ".join( [ ( - f" C {s.cps[1,0]-box_min_x},{box_max_y - s.cps[1,1]}" - f" {s.cps[2,0]-box_min_x},{box_max_y - s.cps[2,1]}" - f" {s.cps[3,0]-box_min_x},{box_max_y - s.cps[3,1]}" + f" C {s.cps[1, 0] - box_min_x},{box_max_y - s.cps[1, 1]}" + f" {s.cps[2, 0] - box_min_x},{box_max_y - s.cps[2, 1]}" + f" {s.cps[3, 0] - box_min_x},{box_max_y - s.cps[3, 1]}" ) for s in bezier_elements ] @@ -1027,7 +1025,7 @@ def _approximate_curve(original_spline, tolerance): # draw path d=path_d, style=( - f"fill:none;stroke:{_rgb_2_hex(r,g,b)};stroke-opacity:{a};" + f"fill:none;stroke:{_rgb_2_hex(r, g, b)};stroke-opacity:{a};" f"stroke-width:{spline.show_options.get('lw', 0.01)};" "stroke-linecap:round" ), @@ -1048,16 +1046,16 @@ def _approximate_curve(original_spline, tolerance): bezier_elements += spline_copy.extract.beziers() path_d = ( - f"M {bezier_elements[0].cps[0,0]-box_min_x}," - f"{box_max_y - bezier_elements[0].cps[0,1]}" + f"M {bezier_elements[0].cps[0, 0] - box_min_x}," + f"{box_max_y - bezier_elements[0].cps[0, 1]}" ) path_d += " ".join( [ ( f" C" - f" {s.cps[1,0]-box_min_x},{box_max_y - s.cps[1,1]}" - f" {s.cps[2,0]-box_min_x},{box_max_y - s.cps[2,1]}" - f" {s.cps[3,0]-box_min_x},{box_max_y - s.cps[3,1]}" + f" {s.cps[1, 0] - box_min_x},{box_max_y - s.cps[1, 1]}" + f" {s.cps[2, 0] - box_min_x},{box_max_y - s.cps[2, 1]}" + f" {s.cps[3, 0] - box_min_x},{box_max_y - s.cps[3, 1]}" ) for s in bezier_elements ] @@ -1069,8 +1067,8 @@ def _approximate_curve(original_spline, tolerance): # draw path d=path_d, style=( - f"fill:{_rgb_2_hex(r,g,b)};fill-opacity:{a};stroke:none;" - f"stroke-linecap:{kwargs.get('linecap','round')}" + f"fill:{_rgb_2_hex(r, g, b)};fill-opacity:{a};stroke:none;" + f"stroke-linecap:{kwargs.get('linecap', 'round')}" ), ) @@ -1094,7 +1092,6 @@ def _approximate_curve(original_spline, tolerance): ukvs = spline.unique_knots[0] knot_projected = spline.evaluate(ukvs.reshape(-1, 1)) for x, y in knot_projected: - _ET.SubElement( svg_knots, "rect", @@ -1103,13 +1100,11 @@ def _approximate_curve(original_spline, tolerance): height=str(lw), width=str(lw), style=( - f"fill:{_rgb_2_hex(r,g,b)};stroke:none;" - f"fill-opacity:{a};" + f"fill:{_rgb_2_hex(r, g, b)};stroke:none;fill-opacity:{a};" ), ) else: - # Extract knot lines as splines knot_lines = [] for knot in spline.unique_knots[0]: @@ -1123,16 +1118,16 @@ def _approximate_curve(original_spline, tolerance): spline_copy = _approximate_curve(knot_line, tolerance) bezier_elements = spline_copy.extract.beziers() path_d = ( - f"M {bezier_elements[0].cps[0,0]-box_min_x}," - f"{box_max_y - bezier_elements[0].cps[0,1]}" + f"M {bezier_elements[0].cps[0, 0] - box_min_x}," + f"{box_max_y - bezier_elements[0].cps[0, 1]}" ) path_d += " ".join( [ ( - f" C {s.cps[1,0]-box_min_x}," - f"{box_max_y - s.cps[1,1]}" - f" {s.cps[2,0]-box_min_x},{box_max_y - s.cps[2,1]}" - f" {s.cps[3,0]-box_min_x},{box_max_y - s.cps[3,1]}" + f" C {s.cps[1, 0] - box_min_x}," + f"{box_max_y - s.cps[1, 1]}" + f" {s.cps[2, 0] - box_min_x},{box_max_y - s.cps[2, 1]}" + f" {s.cps[3, 0] - box_min_x},{box_max_y - s.cps[3, 1]}" ) for s in bezier_elements ] @@ -1143,10 +1138,10 @@ def _approximate_curve(original_spline, tolerance): # draw path d=path_d, style=( - f"fill:none;stroke:{_rgb_2_hex(r,g,b)};" + f"fill:none;stroke:{_rgb_2_hex(r, g, b)};" f"stroke-opacity:{a};" f"stroke-width:{lw};" - f"stroke-linecap:{kwargs.get('linecap','round')}" + f"stroke-linecap:{kwargs.get('linecap', 'round')}" ), ) diff --git a/splinepy/microstructure/microstructure.py b/splinepy/microstructure/microstructure.py index 366935651..33ce9bd89 100644 --- a/splinepy/microstructure/microstructure.py +++ b/splinepy/microstructure/microstructure.py @@ -79,8 +79,7 @@ def deformation_function(self, deformation_function): if not isinstance(deformation_function, _PySpline): raise ValueError( - "Deformation function must be splinepy-Spline." - " e.g. splinepy.NURBS" + "Deformation function must be splinepy-Spline. e.g. splinepy.NURBS" ) self._deformation_function = deformation_function @@ -121,7 +120,7 @@ def tiling(self, tiling): """ if not isinstance(tiling, list) and not isinstance(tiling, int): raise ValueError( - "Tiling mus be either list of integers of integer " "value" + "Tiling mus be either list of integers of integer value" ) self._tiling = tiling # Is defaulted to False using function arguments @@ -263,7 +262,7 @@ def _additional_knots(self, knot_span_wise): # Create Spline that will be used to iterate over parametric space ukvs = self.deformation_function.unique_knots if knot_span_wise: - for tt, ukv in zip(self.tiling, ukvs): + for tt, ukv in zip(self.tiling, ukvs, strict=True): inv_t = 1 / tt new_knots = [ ukv[i - 1] + j * inv_t * (ukv[i] - ukv[i - 1]) @@ -276,7 +275,9 @@ def _additional_knots(self, knot_span_wise): "New knots will be inserted one by one with the objective" " to evenly distribute tiles within the parametric domain" ) - for i_pd, (tt, ukv) in enumerate(zip(self.tiling, ukvs)): + for i_pd, (tt, ukv) in enumerate( + zip(self.tiling, ukvs, strict=True) + ): n_current_spans = len(ukv) - 1 if tt == n_current_spans: continue @@ -618,7 +619,7 @@ def create( for j in anti_support: spline_list_derivs[j].extend(empty_splines) for j, deris in enumerate(derivatives): - for tile_v, tile_deriv in zip(tile, deris): + for tile_v, tile_deriv in zip(tile, deris, strict=True): spline_list_derivs[support[j]].append( def_fun.composition_derivative(tile_v, tile_deriv) ) @@ -723,8 +724,7 @@ def _sanity_check(self): or (self.tiling is None) ): self._logd( - "Current information not sufficient," - " awaiting further assignments" + "Current information not sufficient, awaiting further assignments" ) return False # Check if microtile object fulfils requirements @@ -823,8 +823,7 @@ def __init__(self, microtile): for m in microtile: if not isinstance(m, _PySpline): raise ValueError( - "Microtiles must be (list of) " - "splinepy-Splines. e.g. splinepy.NURBS" + "Microtiles must be (list of) splinepy-Splines. e.g. splinepy.NURBS" ) # Extract beziers for every non Bezier patch else this just # returns itself diff --git a/splinepy/microstructure/tiles/__init__.py b/splinepy/microstructure/tiles/__init__.py index 3b35a1f5c..9404d79e3 100644 --- a/splinepy/microstructure/tiles/__init__.py +++ b/splinepy/microstructure/tiles/__init__.py @@ -86,7 +86,7 @@ def _summarize_tiles(): key = SubClass.__qualname__ # save types and sort with direction tile_types[key] = SubClass - dim = SubClass.dim + dim = SubClass._dim if dim == 1: d1[key] = SubClass elif dim == 2: @@ -123,7 +123,7 @@ def by_dim(para_dim=None, dim=None): para_dim = int(para_dim) filtered = {} for key, value in pool.items(): - if value.para_dim == para_dim: + if value._para_dim == para_dim: filtered[key] = value # overwrite pool with filtered diff --git a/splinepy/utils/data.py b/splinepy/utils/data.py index 4c66e716d..00105c796 100644 --- a/splinepy/utils/data.py +++ b/splinepy/utils/data.py @@ -468,7 +468,7 @@ def uniform_query(bounds, resolutions): # create per-dimension queries queries_per_dim = [] - for lb, ub, r in zip(lower_b, upper_b, resolutions): + for lb, ub, r in zip(lower_b, upper_b, resolutions, strict=True): queries_per_dim.append(_np.linspace(lb, ub, r)) return cartesian_product(queries_per_dim, reverse=True) @@ -573,8 +573,7 @@ def __init__( # can call sample or has a function? if not self.has_function and not self.is_spline: raise ValueError( - "None spline data should at least have an accompanying " - "function." + "None spline data should at least have an accompanying function." ) def as_vertex_data(self, resolutions=None, on=None): @@ -595,8 +594,7 @@ def as_vertex_data(self, resolutions=None, on=None): if self.has_locations and (resolutions is not None or on is not None): raise ValueError( - "Location dependent data can't be evaluated with `resolutions`" - " or `at`." + "Location dependent data can't be evaluated with `resolutions` or `at`." ) # if resolutions is specified, this is not a location query diff --git a/tests/conftest.py b/tests/conftest.py index 5043ef971..b4381441a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -222,7 +222,9 @@ def _raster(bounds, resolutions): pts = np.meshgrid( *[ np.linspace(lo, up, re) - for lo, up, re in zip(l_bounds, u_bounds, resolutions) + for lo, up, re in zip( + l_bounds, u_bounds, resolutions, strict=True + ) ], indexing="ij", ) @@ -270,7 +272,7 @@ def _are_splines_equal(a, b, print_=False): return False for req_prop in a.required_properties: if req_prop == "knot_vectors": - for aa, bb in zip(a.knot_vectors, b.knot_vectors): + for aa, bb in zip(a.knot_vectors, b.knot_vectors, strict=True): if not np.allclose(aa.numpy(), bb.numpy()): if print_: error_log("a.kvs", a.kvs) @@ -292,7 +294,7 @@ def _are_items_close(a, b): """returns True if items in a and b are close""" all_close = True - for i, (aa, bb) in enumerate(zip(a, b)): + for i, (aa, bb) in enumerate(zip(a, b, strict=True)): if not all(np.isclose(aa, bb)): # print to inform error_log(f"elements in index-{i} are not close") @@ -312,7 +314,7 @@ def _are_items_same(a, b): """returns True if items in a and b are same""" all_same = True - for i, (aa, bb) in enumerate(zip(a, b)): + for i, (aa, bb) in enumerate(zip(a, b, strict=True)): if aa != bb: # print to inform error_log(f"element in index-{i} are not same") @@ -332,8 +334,7 @@ def _are_stripped_lines_same(a, b, ignore_order=False): """returns True if items in a and b same, preceding and tailing whitespaces are ignored and strings are joined""" all_same = True - - for i, (line_a, line_b) in enumerate(zip(a, b)): + for i, (line_a, line_b) in enumerate(zip(a, b, strict=True)): # check stripped string stripped_a, stripped_b = line_a.strip(), line_b.strip() diff --git a/tests/data/mfem_cartesian_2d.mesh b/tests/data/mfem_cartesian_2d.mesh index b70d4a28a..746ffe2b6 100644 --- a/tests/data/mfem_cartesian_2d.mesh +++ b/tests/data/mfem_cartesian_2d.mesh @@ -85,3 +85,4 @@ controlpoints_cartesian 2.0 0.0 1.0 1.0 1.0 1.0 2.0 1.0 1.0 + diff --git a/tests/data/mfem_cartesian_3d.mesh b/tests/data/mfem_cartesian_3d.mesh index 389d3928d..b6bc475cb 100644 --- a/tests/data/mfem_cartesian_3d.mesh +++ b/tests/data/mfem_cartesian_3d.mesh @@ -147,3 +147,4 @@ controlpoints_cartesian 2.0 0.0 1.0 1.0 1.0 1.0 1.0 1.0 2.0 1.0 1.0 1.0 + diff --git a/tests/helpme/test_create.py b/tests/helpme/test_create.py index b31ae441c..8ec877be2 100644 --- a/tests/helpme/test_create.py +++ b/tests/helpme/test_create.py @@ -182,7 +182,7 @@ def check_parametric_view(spline, conform): # same knot_vectors if spline.has_knot_vectors: - for p_kv, kv in zip(p_spl.kvs, spline.kvs): + for p_kv, kv in zip(p_spl.kvs, spline.kvs, strict=True): assert np.allclose(p_kv, kv) # same weights @@ -194,7 +194,9 @@ def check_parametric_view(spline, conform): assert not any(p_spl.ds - 1) # same unique knots - implies same p_bounds - for p_ukv, ukv in zip(p_spl.unique_knots, spline.unique_knots): + for p_ukv, ukv in zip( + p_spl.unique_knots, spline.unique_knots, strict=True + ): assert np.allclose(p_ukv, ukv) spl = request.getfixturevalue(splinetype) diff --git a/tests/io/test_cats.py b/tests/io/test_cats.py index 46d835cbf..dd06cda7a 100644 --- a/tests/io/test_cats.py +++ b/tests/io/test_cats.py @@ -259,6 +259,7 @@ def test_cats_import(to_tmpf, are_splines_equal): for a, b in zip( multipatch_geometry, multipatch_geometry_loaded, + strict=True, ) ) @@ -276,5 +277,6 @@ def test_cats_import(to_tmpf, are_splines_equal): for a, b in zip( multipatch_geometry, multipatch_geometry_loaded, + strict=True, ) ) diff --git a/tests/io/test_gismo.py b/tests/io/test_gismo.py index 4376f41d0..80ae3531c 100644 --- a/tests/io/test_gismo.py +++ b/tests/io/test_gismo.py @@ -160,10 +160,13 @@ def test_gismo_export_2D( labeled_boundaries=False, ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_noindent_nolabels_ascii_2d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_noindent_nolabels_ascii_2d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -183,10 +186,13 @@ def test_gismo_export_2D_indented( labeled_boundaries=False, ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_indent_nolabels_ascii_2d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_indent_nolabels_ascii_2d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -207,10 +213,13 @@ def test_gismo_export_2D_labels( labeled_boundaries=True, ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_noindent_labels_ascii_2d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_noindent_labels_ascii_2d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -230,10 +239,13 @@ def test_gismo_export_2D_labels_indented( labeled_boundaries=True, ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_indent_labels_ascii_2d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_indent_labels_ascii_2d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -254,10 +266,13 @@ def test_gismo_export_3D( labeled_boundaries=False, ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_noindent_nolabels_ascii_3d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_noindent_nolabels_ascii_3d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -324,10 +339,13 @@ def test_gismo_export_additional_blocks( additional_blocks=additional_blocks.to_list(), ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_additional_blocks.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_additional_blocks.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -347,10 +365,13 @@ def test_gismo_export_3D_indented( labeled_boundaries=False, ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) - + "/../data/gismo_indent_nolabels_ascii_3d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + + "/../data/gismo_indent_nolabels_ascii_3d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -399,6 +420,7 @@ def test_gismo_import(to_tmpf, are_splines_equal): for a, b in zip( multipatch_geometry.patches, multipatch_geometry_loaded.patches, + strict=True, ) ) @@ -425,6 +447,7 @@ def test_gismo_import(to_tmpf, are_splines_equal): for a, b in zip( multipatch_geometry.patches, multipatch_geometry_loaded.patches, + strict=True, ) ) @@ -505,6 +528,7 @@ def test_gismo_import_with_options(to_tmpf, are_splines_equal): for a, b in zip( multipatch_geometry.patches, multipatch_geometry_loaded.patches, + strict=True, ) ) @@ -552,10 +576,13 @@ def test_gismo_io_binary(to_tmpf, are_stripped_lines_same, are_splines_equal): gismo_options_loaded, ) = splinepy.io.gismo.load(tmpf, load_options=True) - with open(tmpf) as tmp_read, open( - os.path.dirname(os.path.dirname(__file__)) - + "/data/gismo_noindent_nolabels_b64_3d.xml" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(os.path.dirname(__file__)) + + "/data/gismo_noindent_nolabels_b64_3d.xml" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -565,6 +592,7 @@ def test_gismo_io_binary(to_tmpf, are_stripped_lines_same, are_splines_equal): for a, b in zip( multipatch_geometry.patches, multipatch_geometry_loaded.patches, + strict=True, ) ) diff --git a/tests/io/test_iges.py b/tests/io/test_iges.py index 7b3fb2be5..6aed23afb 100644 --- a/tests/io/test_iges.py +++ b/tests/io/test_iges.py @@ -56,5 +56,5 @@ def test_iges_round_trip_bsplines(to_tmpf, are_splines_equal): assert all( are_splines_equal(a, b) - for a, b in zip(splines_in_3d, list_of_splines_loaded) + for a, b in zip(splines_in_3d, list_of_splines_loaded, strict=True) ) diff --git a/tests/io/test_irit.py b/tests/io/test_irit.py index ea28d8c6e..be56418ec 100644 --- a/tests/io/test_irit.py +++ b/tests/io/test_irit.py @@ -25,5 +25,7 @@ def test_irit_export_import( list_of_splines_loaded = splinepy.io.irit.load(tmpf) assert all( are_splines_equal(a, b) - for a, b in zip(list_of_splines, list_of_splines_loaded) + for a, b in zip( + list_of_splines, list_of_splines_loaded, strict=True + ) ) diff --git a/tests/io/test_json.py b/tests/io/test_json.py index e4571655e..8016da122 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -43,7 +43,9 @@ def test_json_import(to_tmpf, are_splines_equal): list_of_splines_loaded = splinepy.io.json.load(tmpf) assert all( are_splines_equal(a, b) - for a, b in zip(list_of_splines, list_of_splines_loaded) + for a, b in zip( + list_of_splines, list_of_splines_loaded, strict=True + ) ) # Test Import export with non base64 encoding @@ -53,5 +55,7 @@ def test_json_import(to_tmpf, are_splines_equal): list_of_splines_loaded = splinepy.io.json.load(tmpf) assert all( are_splines_equal(a, b) - for a, b in zip(list_of_splines, list_of_splines_loaded) + for a, b in zip( + list_of_splines, list_of_splines_loaded, strict=True + ) ) diff --git a/tests/io/test_mfem_export.py b/tests/io/test_mfem_export.py index 5c2544a94..4617a340f 100644 --- a/tests/io/test_mfem_export.py +++ b/tests/io/test_mfem_export.py @@ -43,9 +43,12 @@ def test_mfem_export(to_tmpf, are_stripped_lines_same): tmpf, [bez_el0, bsp_el2, nur_el3, rbz_el1] ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) + "/../data/mfem_cartesian_2d.mesh" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + "/../data/mfem_cartesian_2d.mesh" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) @@ -123,9 +126,12 @@ def test_mfem_export(to_tmpf, are_stripped_lines_same): tmpf, [bez_el0, bsp_el2, nur_el3, rbz_el1] ) - with open(tmpf) as tmp_read, open( - os.path.dirname(__file__) + "/../data/mfem_cartesian_3d.mesh" - ) as base_file: + with ( + open(tmpf) as tmp_read, + open( + os.path.dirname(__file__) + "/../data/mfem_cartesian_3d.mesh" + ) as base_file, + ): assert are_stripped_lines_same( base_file.readlines(), tmp_read.readlines(), True ) diff --git a/tests/io/test_npz.py b/tests/io/test_npz.py index 7a16f194e..600f14e47 100644 --- a/tests/io/test_npz.py +++ b/tests/io/test_npz.py @@ -43,5 +43,7 @@ def test_npz_io(to_tmpf, are_splines_equal): list_of_splines_loaded = splinepy.io.npz.load(tmpf + ".npz") assert all( are_splines_equal(a, b) - for a, b in zip(list_of_splines, list_of_splines_loaded) + for a, b in zip( + list_of_splines, list_of_splines_loaded, strict=True + ) ) diff --git a/tests/test_bezier_extraction.py b/tests/test_bezier_extraction.py index 49c9cd5ae..5fb137118 100644 --- a/tests/test_bezier_extraction.py +++ b/tests/test_bezier_extraction.py @@ -52,7 +52,7 @@ def test_extraction_matrices(splinetype, np_rng, request): n_matrices = spline.knot_insertion_matrix(beziers=True) beziers_n = spline.extract_bezier_patches() - for m, b in zip(n_matrices, beziers_n): + for m, b in zip(n_matrices, beziers_n, strict=True): # Test matrices m against spline ctps if "nurbs" in splinetype: assert np.allclose(b.weights, m @ spline.weights) diff --git a/tests/test_bezier_operations.py b/tests/test_bezier_operations.py index 5afc97c80..4cb53cf2e 100644 --- a/tests/test_bezier_operations.py +++ b/tests/test_bezier_operations.py @@ -98,7 +98,7 @@ def test_composition_sensitivities_on_bsplines(bspline_2p2d): composed_der_ctps = [] beziers = [] - for bez, mat in zip(extract_beziers, extraction_matrices): + for bez, mat in zip(extract_beziers, extraction_matrices, strict=True): # Composition composed, derivatives = bez.compose( inner_function, compute_sensitivities=True @@ -118,7 +118,7 @@ def test_composition_sensitivities_on_bsplines(bspline_2p2d): # Extract Beziers extract_beziers_dx = bspline_dx.extract_bezier_patches() for bez, bez_dx, comps in zip( - beziers, extract_beziers_dx, composed_der_ctps + beziers, extract_beziers_dx, composed_der_ctps, strict=True ): # Compose finite differences spline composed_dx = bez_dx.compose(inner_function) diff --git a/tests/test_knot_vectors.py b/tests/test_knot_vectors.py index 329d165b5..a16c2fdd3 100644 --- a/tests/test_knot_vectors.py +++ b/tests/test_knot_vectors.py @@ -12,7 +12,7 @@ def test_knot_vectors(splinetype, request): copy_knot_vectors = spline.knot_vectors[:] unique_knots = [np.unique(ckvs) for ckvs in copy_knot_vectors] - for uk, uk_fct in zip(unique_knots, spline.unique_knots): + for uk, uk_fct in zip(unique_knots, spline.unique_knots, strict=True): assert np.allclose(uk, uk_fct) # test knot_multiplicities @@ -20,7 +20,9 @@ def test_knot_vectors(splinetype, request): np.unique(ckvs, return_counts=True) for ckvs in copy_knot_vectors ] - for m_u, m_fct in zip(multiplicity, spline.knot_multiplicities): + for m_u, m_fct in zip( + multiplicity, spline.knot_multiplicities, strict=True + ): assert np.allclose(m_u[1], m_fct) # test knot_vector creation @@ -28,5 +30,6 @@ def test_knot_vectors(splinetype, request): spline.unique_knots, spline.knot_multiplicities, spline.knot_vectors, + strict=True, ): assert np.allclose(np.array(spl_kv), np.repeat(u_kv, kn_m)) diff --git a/tests/test_kv_manipulation.py b/tests/test_kv_manipulation.py index b3c7d63da..04516d5aa 100644 --- a/tests/test_kv_manipulation.py +++ b/tests/test_kv_manipulation.py @@ -196,7 +196,7 @@ def test_uniform_refine(): # compute what it should be n_elem2_ref = 1 - for uk, nk in zip(unique_knots1, n_knots): + for uk, nk in zip(unique_knots1, n_knots, strict=True): n_elem2_ref *= (len(uk) - 1) * (nk + 1) assert n_elem2_ref == n_elem2 diff --git a/tests/test_multipatch.py b/tests/test_multipatch.py index b2ad9cc38..51390a23a 100644 --- a/tests/test_multipatch.py +++ b/tests/test_multipatch.py @@ -207,11 +207,11 @@ def test_interfaces_and_boundaries(are_splines_equal): bmp_1 = multipatch.boundary_multipatch(1) assert len(bmp_1.patches) == 2 boundary_1 = [] - for i_patch, i_face in zip(*multipatch.boundaries[0]): + for i_patch, i_face in zip(*multipatch.boundaries[0], strict=True): boundary_1.append( *multipatch.patches[i_patch].extract.boundaries([i_face]) ) - for patch_0, patch_1 in zip(boundary_1, bmp_1.patches): + for patch_0, patch_1 in zip(boundary_1, bmp_1.patches, strict=True): assert are_splines_equal(patch_0, patch_1) assert len(multipatch.boundary_patch_ids(8)) == 0 diff --git a/tests/test_normalize_knot_vectors.py b/tests/test_normalize_knot_vectors.py index ced5ab685..494a3a6db 100644 --- a/tests/test_normalize_knot_vectors.py +++ b/tests/test_normalize_knot_vectors.py @@ -14,7 +14,9 @@ def test_bspline_normalize_knot_vectors(bspline_2p2d): # normalize bspline.normalize_knot_vectors() - for i, (ref_kv, kv) in enumerate(zip(ref, bspline.knot_vectors)): + for i, (ref_kv, kv) in enumerate( + zip(ref, bspline.knot_vectors, strict=True) + ): assert np.allclose(ref_kv, kv), f"{i}. para dim failed to normalize" @@ -31,5 +33,7 @@ def test_nurbs_normalize_knot_vectors(nurbs_2p2d): # normalize nurbs.normalize_knot_vectors() - for i, (ref_kv, kv) in enumerate(zip(ref, nurbs.knot_vectors)): + for i, (ref_kv, kv) in enumerate( + zip(ref, nurbs.knot_vectors, strict=True) + ): assert np.allclose(ref_kv, kv), f"{i}. para dim failed to normalize" From e6f49e74c05527adcc20b4c49b5bc80dd8192281 Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Tue, 14 Apr 2026 15:36:17 +0200 Subject: [PATCH 075/171] Chore(pre-commit): Change pre-commit ci target branch --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2ccf8deb..1e9b8be63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,8 @@ # use default options for ci ci: - autoupdate_schedule: "weekly" + autoupdate_branch: 'develop' + autoupdate_schedule: "monthly" submodules: false repos: From 9c4772479cf25f233617f574dfc30607ababe8df Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 9 Aug 2024 15:42:48 +0200 Subject: [PATCH 076/171] Add test for microtile classes --- splinepy/microstructure/microstructure.py | 4 +- splinepy/microstructure/tiles/__init__.py | 2 + splinepy/microstructure/tiles/armadillo.py | 9 + splinepy/microstructure/tiles/chi.py | 14 +- splinepy/microstructure/tiles/cross_2d.py | 15 +- splinepy/microstructure/tiles/cross_3d.py | 6 + .../microstructure/tiles/cross_3d_linear.py | 2 + splinepy/microstructure/tiles/cube_void.py | 1 + .../microstructure/tiles/double_lattice.py | 3 +- splinepy/microstructure/tiles/ellips_v_oid.py | 1 + splinepy/microstructure/tiles/hollow_cube.py | 1 + .../microstructure/tiles/hollow_octagon.py | 2 + .../tiles/hollow_octagon_extrude.py | 4 +- .../microstructure/tiles/inverse_cross_3d.py | 556 +++++++++--------- splinepy/microstructure/tiles/snappy.py | 4 +- tests/test_microstructure.py | 75 +++ 16 files changed, 412 insertions(+), 287 deletions(-) create mode 100644 tests/test_microstructure.py diff --git a/splinepy/microstructure/microstructure.py b/splinepy/microstructure/microstructure.py index 33ce9bd89..d93198f95 100644 --- a/splinepy/microstructure/microstructure.py +++ b/splinepy/microstructure/microstructure.py @@ -9,7 +9,7 @@ class Microstructure(_SplinepyBase): - """Helper class to facilitatae the construction of microstructures.""" + """Helper class to facilitate the construction of microstructures.""" def __init__( self, @@ -18,7 +18,7 @@ def __init__( microtile=None, parametrization_function=None, ): - """Helper class to facilitatae the construction of microstructures. + """Helper class to facilitate the construction of microstructures. Parameters ---------- diff --git a/splinepy/microstructure/tiles/__init__.py b/splinepy/microstructure/tiles/__init__.py index 9404d79e3..d301320e4 100644 --- a/splinepy/microstructure/tiles/__init__.py +++ b/splinepy/microstructure/tiles/__init__.py @@ -8,6 +8,7 @@ armadillo, chi, cross_2d, + cross_3d, cross_3d_linear, cube_void, double_lattice, @@ -42,6 +43,7 @@ "chi", "cross_2d", "cube_void", + "cross_3d", "cross_3d_linear", "double_lattice", "ellips_v_oid", diff --git a/splinepy/microstructure/tiles/armadillo.py b/splinepy/microstructure/tiles/armadillo.py index e7f39aa66..9bbf602da 100644 --- a/splinepy/microstructure/tiles/armadillo.py +++ b/splinepy/microstructure/tiles/armadillo.py @@ -20,6 +20,15 @@ class Armadillo(_TileBase): _dim = 3 _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 1 + _sensitivities_implemented = False + _closure_directions = [ + "x_min", + "x_max", + "y_min", + "y_max", + "z_min", + "z_max", + ] def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/chi.py b/splinepy/microstructure/tiles/chi.py index b8430d0dc..e3cbf708b 100644 --- a/splinepy/microstructure/tiles/chi.py +++ b/splinepy/microstructure/tiles/chi.py @@ -18,6 +18,9 @@ class Chi(_TileBase): _para_dim = 1 _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 1 + _sensitivities_implemented = True + + _parameter_bounds = {"parameters": [-_np.pi / 2, _np.pi / 2]} def create_tile( self, @@ -48,11 +51,16 @@ def create_tile( ) parameters = _np.array([[_np.pi / 8]]) else: + angle_bounds = self._parameter_bounds["parameters"] if not ( - _np.all(parameters >= -_np.pi * 0.5) - and _np.all(parameters < _np.pi * 0.5) + _np.all(parameters >= angle_bounds[0]) + and _np.all(parameters < angle_bounds[1]) ): - raise ValueError("The parameter must be in -Pi/2 and Pi/2") + error_message = ( + f"The parameter must be in {angle_bounds[0]}" + + f"and {angle_bounds[1]}" + ) + raise ValueError(error_message) pass self.check_params(parameters) diff --git a/splinepy/microstructure/tiles/cross_2d.py b/splinepy/microstructure/tiles/cross_2d.py index 083d0c65a..cf4a988d2 100644 --- a/splinepy/microstructure/tiles/cross_2d.py +++ b/splinepy/microstructure/tiles/cross_2d.py @@ -26,6 +26,9 @@ class Cross2D(_TileBase): ] ) _n_info_per_eval_point = 1 + _parameter_bounds = {"center_expansion": [0.5, 1.5]} + _sensitivities_implemented = True + _closure_directions = ["x_min", "x_max", "y_min", "y_max"] def _closing_tile( self, @@ -417,8 +420,16 @@ def create_tile( if not isinstance(center_expansion, float): raise ValueError("Invalid Type") - if not ((center_expansion > 0.5) and (center_expansion < 1.5)): - raise ValueError("Center Expansion must be in (.5,1.5)") + center_expansion_bounds = self._parameter_bounds["center_expansion"] + if not ( + (center_expansion > center_expansion_bounds[0]) + and (center_expansion < center_expansion_bounds[1]) + ): + error_message = ( + "Center Expansion must be in (" + + f"{center_expansion_bounds[0]}, {center_expansion_bounds[1]})" + ) + raise ValueError(error_message) max_radius = min(0.5, (0.5 / center_expansion)) diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index 8752c9881..01282c6f3 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -28,6 +28,8 @@ class Cross3D(_TileBase): ] ) _n_info_per_eval_point = 1 + _sensitivities_implemented = True + _closure_directions = ["z_min", "z_max"] def _closing_tile( self, @@ -557,6 +559,10 @@ def create_tile( derivatives = None if closure is not None: + if closure not in self._closure_directions: + raise NotImplementedError( + f"Closure '{closure}' not implemented" + ) return self._closing_tile( parameters=parameters, parameter_sensitivities=parameter_sensitivities, diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index 3e5901371..952738afe 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -28,6 +28,8 @@ class Cross3DLinear(_TileBase): ] ) _n_info_per_eval_point = 1 + _sensitivities_implemented = True + _closure_directions = ["z_min", "z_max"] def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/cube_void.py b/splinepy/microstructure/tiles/cube_void.py index be4f1ab3a..a8da85df6 100644 --- a/splinepy/microstructure/tiles/cube_void.py +++ b/splinepy/microstructure/tiles/cube_void.py @@ -25,6 +25,7 @@ class CubeVoid(_TileBase): _para_dim = 3 _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 4 + _sensitivities_implemented = True # Aux values _sphere_ctps = _np.array( diff --git a/splinepy/microstructure/tiles/double_lattice.py b/splinepy/microstructure/tiles/double_lattice.py index ad8f6e803..ffb2166b2 100644 --- a/splinepy/microstructure/tiles/double_lattice.py +++ b/splinepy/microstructure/tiles/double_lattice.py @@ -20,6 +20,7 @@ class DoubleLattice(_TileBase): _para_dim = 2 _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 2 + _sensitivities_implemented = True def create_tile( self, @@ -44,7 +45,7 @@ def create_tile( correlates with thickness of branches and entouring wall contact_length : double required for conformity between tiles, sets the length of the center - block on the tiles boundary + block on the tile's boundary Returns ------- diff --git a/splinepy/microstructure/tiles/ellips_v_oid.py b/splinepy/microstructure/tiles/ellips_v_oid.py index 4b6e7bf23..ec7a5cfe9 100644 --- a/splinepy/microstructure/tiles/ellips_v_oid.py +++ b/splinepy/microstructure/tiles/ellips_v_oid.py @@ -27,6 +27,7 @@ class EllipsVoid(_TileBase): _para_dim = 3 _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 4 + _sensitivities_implemented = True # Aux values _c0 = 0.5 / 3**0.5 diff --git a/splinepy/microstructure/tiles/hollow_cube.py b/splinepy/microstructure/tiles/hollow_cube.py index fa8c15795..7b37daa68 100644 --- a/splinepy/microstructure/tiles/hollow_cube.py +++ b/splinepy/microstructure/tiles/hollow_cube.py @@ -28,6 +28,7 @@ class HollowCube(_TileBase): ] ) _n_info_per_eval_point = 1 + _sensitivities_implemented = True def create_tile( self, diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index 20a628e17..44b31a65a 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -18,6 +18,8 @@ class HollowOctagon(_TileBase): _para_dim = 2 _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 1 + _sensitivities_implemented = False + _closure_directions = ["x_min", "x_max", "y_min", "y_max"] def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index 82e738d23..dbf5f1901 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -18,6 +18,8 @@ class HollowOctagonExtrude(_TileBase): _para_dim = 3 _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 1 + _sensitivities_implemented = False + _closure_directions = ["x_min", "x_max", "y_min", "y_max"] def create_tile( self, @@ -215,7 +217,7 @@ def create_tile( return (spline_list, None) - def closing_tile( + def _closing_tile( self, parameters=None, parameter_sensitivities=None, # TODO diff --git a/splinepy/microstructure/tiles/inverse_cross_3d.py b/splinepy/microstructure/tiles/inverse_cross_3d.py index c24954702..a633632fc 100644 --- a/splinepy/microstructure/tiles/inverse_cross_3d.py +++ b/splinepy/microstructure/tiles/inverse_cross_3d.py @@ -32,6 +32,8 @@ class InverseCross3D(_TileBase): ] ) _n_info_per_eval_point = 1 + _sensitivities_implemented = False + _closure_directions = ["z_min", "z_max"] def _closing_tile( self, @@ -40,7 +42,7 @@ def _closing_tile( closure=None, boundary_width=0.1, filling_height=0.5, - seperator_distance=None, + separator_distance=None, **kwargs, # noqa ARG002 ): """Create a closing tile to match with closed surface. @@ -53,7 +55,7 @@ def _closing_tile( with of the boundary surrounding branch filling_height : float portion of the height that is filled in parametric domain - seperator_distance : float + separator_distance : float Describes the position of the separator layer in the control point domain closure : str @@ -70,8 +72,8 @@ def _closing_tile( raise ValueError("No closing direction given") # Set default values - if seperator_distance is None: - seperator_distance = 0.3 + if separator_distance is None: + separator_distance = 0.3 if parameters is None: self._logd("Tile request is not parametrized, setting default 0.2") @@ -108,7 +110,7 @@ def _closing_tile( center_width = 1.0 - 2 * boundary_width r_center = center_width * 0.5 half_r_center = (r_center + 0.5) * 0.5 - aux_column_width = 0.5 - 2 * (0.5 - seperator_distance) + aux_column_width = 0.5 - 2 * (0.5 - separator_distance) spline_list = [] if closure == "z_min": @@ -123,7 +125,7 @@ def _closing_tile( [-r_center, r_center, filling_height], [-0.5, -aux_column_width, ctps_mid_height_top], [ - -seperator_distance, + -separator_distance, -aux_column_width, ctps_mid_height_top, ], @@ -134,16 +136,16 @@ def _closing_tile( ], [-0.5, aux_column_width, ctps_mid_height_top], [ - -seperator_distance, + -separator_distance, aux_column_width, ctps_mid_height_top, ], [-branch_thickness, branch_thickness, ctps_mid_height_top], [-0.5, -aux_column_width, 1.0], - [-seperator_distance, -aux_column_width, 1.0], + [-separator_distance, -aux_column_width, 1.0], [-branch_thickness, -branch_thickness, 1.0], [-0.5, aux_column_width, 1.0], - [-seperator_distance, aux_column_width, 1.0], + [-separator_distance, aux_column_width, 1.0], [-branch_thickness, branch_thickness, 1.0], ] ) + _np.array([0.5, 0.5, 0.0]) @@ -165,23 +167,23 @@ def _closing_tile( [0.5, r_center, filling_height], [branch_thickness, -branch_thickness, ctps_mid_height_top], [ - seperator_distance, + separator_distance, -aux_column_width, ctps_mid_height_top, ], [0.5, -aux_column_width, ctps_mid_height_top], [branch_thickness, branch_thickness, ctps_mid_height_top], [ - seperator_distance, + separator_distance, aux_column_width, ctps_mid_height_top, ], [0.5, aux_column_width, ctps_mid_height_top], [branch_thickness, -branch_thickness, 1.0], - [seperator_distance, -aux_column_width, 1.0], + [separator_distance, -aux_column_width, 1.0], [0.5, -aux_column_width, 1.0], [branch_thickness, branch_thickness, 1.0], - [seperator_distance, aux_column_width, 1.0], + [separator_distance, aux_column_width, 1.0], [0.5, aux_column_width, 1.0], ] ) + _np.array([0.5, 0.5, 0.0]) @@ -205,12 +207,12 @@ def _closing_tile( [aux_column_width, -0.5, ctps_mid_height_top], [ -aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_top, ], [ aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_top, ], [ @@ -221,8 +223,8 @@ def _closing_tile( [branch_thickness, -branch_thickness, ctps_mid_height_top], [-aux_column_width, -0.5, 1.0], [aux_column_width, -0.5, 1.0], - [-aux_column_width, -seperator_distance, 1.0], - [aux_column_width, -seperator_distance, 1.0], + [-aux_column_width, -separator_distance, 1.0], + [aux_column_width, -separator_distance, 1.0], [-branch_thickness, -branch_thickness, 1.0], [branch_thickness, -branch_thickness, 1.0], ] @@ -247,20 +249,20 @@ def _closing_tile( [branch_thickness, branch_thickness, ctps_mid_height_top], [ -aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_top, ], [ aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_top, ], [-aux_column_width, 0.5, ctps_mid_height_top], [aux_column_width, 0.5, ctps_mid_height_top], [-branch_thickness, branch_thickness, 1.0], [branch_thickness, branch_thickness, 1.0], - [-aux_column_width, seperator_distance, 1.0], - [aux_column_width, seperator_distance, 1.0], + [-aux_column_width, separator_distance, 1.0], + [aux_column_width, separator_distance, 1.0], [-aux_column_width, 0.5, 1.0], [aux_column_width, 0.5, 1.0], ] @@ -285,22 +287,22 @@ def _closing_tile( [-half_r_center, -r_center, filling_height], [-r_center, -r_center, filling_height], [-0.5, -0.5, ctps_mid_height_top], - [-seperator_distance, -0.5, ctps_mid_height_top], + [-separator_distance, -0.5, ctps_mid_height_top], [-aux_column_width, -0.5, ctps_mid_height_top], - [-0.5, -seperator_distance, ctps_mid_height_top], + [-0.5, -separator_distance, ctps_mid_height_top], [ - -seperator_distance, - -seperator_distance, + -separator_distance, + -separator_distance, ctps_mid_height_top, ], [ -aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_top, ], [-0.5, -aux_column_width, ctps_mid_height_top], [ - -seperator_distance, + -separator_distance, -aux_column_width, ctps_mid_height_top, ], @@ -310,13 +312,13 @@ def _closing_tile( ctps_mid_height_top, ], [-0.5, -0.5, 1.0], - [-seperator_distance, -0.5, 1.0], + [-separator_distance, -0.5, 1.0], [-aux_column_width, -0.5, 1.0], - [-0.5, -seperator_distance, 1.0], - [-seperator_distance, -seperator_distance, 1.0], - [-aux_column_width, -seperator_distance, 1.0], + [-0.5, -separator_distance, 1.0], + [-separator_distance, -separator_distance, 1.0], + [-aux_column_width, -separator_distance, 1.0], [-0.5, -aux_column_width, 1.0], - [-seperator_distance, -aux_column_width, 1.0], + [-separator_distance, -aux_column_width, 1.0], [-branch_thickness, -branch_thickness, 1.0], ] ) + _np.array([0.5, 0.5, 0.0]) @@ -340,33 +342,33 @@ def _closing_tile( [-r_center, 0.5, filling_height], [-0.5, aux_column_width, ctps_mid_height_top], [ - -seperator_distance, + -separator_distance, aux_column_width, ctps_mid_height_top, ], [-branch_thickness, branch_thickness, ctps_mid_height_top], - [-0.5, seperator_distance, ctps_mid_height_top], + [-0.5, separator_distance, ctps_mid_height_top], [ - -seperator_distance, - seperator_distance, + -separator_distance, + separator_distance, ctps_mid_height_top, ], [ -aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_top, ], [-0.5, 0.5, ctps_mid_height_top], - [-seperator_distance, 0.5, ctps_mid_height_top], + [-separator_distance, 0.5, ctps_mid_height_top], [-aux_column_width, 0.5, ctps_mid_height_top], [-0.5, aux_column_width, 1.0], - [-seperator_distance, aux_column_width, 1.0], + [-separator_distance, aux_column_width, 1.0], [-branch_thickness, branch_thickness, 1.0], - [-0.5, seperator_distance, 1.0], - [-seperator_distance, seperator_distance, 1.0], - [-aux_column_width, seperator_distance, 1.0], + [-0.5, separator_distance, 1.0], + [-separator_distance, separator_distance, 1.0], + [-aux_column_width, separator_distance, 1.0], [-0.5, 0.5, 1.0], - [-seperator_distance, 0.5, 1.0], + [-separator_distance, 0.5, 1.0], [-aux_column_width, 0.5, 1.0], ] ) + _np.array([0.5, 0.5, 0.0]) @@ -389,34 +391,34 @@ def _closing_tile( [half_r_center, -r_center, filling_height], [0.5, -r_center, filling_height], [aux_column_width, -0.5, ctps_mid_height_top], - [seperator_distance, -0.5, ctps_mid_height_top], + [separator_distance, -0.5, ctps_mid_height_top], [0.5, -0.5, ctps_mid_height_top], [ aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_top, ], [ - seperator_distance, - -seperator_distance, + separator_distance, + -separator_distance, ctps_mid_height_top, ], - [0.5, -seperator_distance, ctps_mid_height_top], + [0.5, -separator_distance, ctps_mid_height_top], [branch_thickness, -branch_thickness, ctps_mid_height_top], [ - seperator_distance, + separator_distance, -aux_column_width, ctps_mid_height_top, ], [0.5, -aux_column_width, ctps_mid_height_top], [aux_column_width, -0.5, 1.0], - [seperator_distance, -0.5, 1.0], + [separator_distance, -0.5, 1.0], [0.5, -0.5, 1.0], - [aux_column_width, -seperator_distance, 1.0], - [seperator_distance, -seperator_distance, 1.0], - [0.5, -seperator_distance, 1.0], + [aux_column_width, -separator_distance, 1.0], + [separator_distance, -separator_distance, 1.0], + [0.5, -separator_distance, 1.0], [branch_thickness, -branch_thickness, 1.0], - [seperator_distance, -aux_column_width, 1.0], + [separator_distance, -aux_column_width, 1.0], [0.5, -aux_column_width, 1.0], ] ) + _np.array([0.5, 0.5, 0.0]) @@ -440,33 +442,33 @@ def _closing_tile( [0.5, 0.5, filling_height], [branch_thickness, branch_thickness, ctps_mid_height_top], [ - seperator_distance, + separator_distance, aux_column_width, ctps_mid_height_top, ], [0.5, aux_column_width, ctps_mid_height_top], [ aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_top, ], [ - seperator_distance, - seperator_distance, + separator_distance, + separator_distance, ctps_mid_height_top, ], - [0.5, seperator_distance, ctps_mid_height_top], + [0.5, separator_distance, ctps_mid_height_top], [aux_column_width, 0.5, ctps_mid_height_top], - [seperator_distance, 0.5, ctps_mid_height_top], + [separator_distance, 0.5, ctps_mid_height_top], [0.5, 0.5, ctps_mid_height_top], [branch_thickness, branch_thickness, 1.0], - [seperator_distance, aux_column_width, 1.0], + [separator_distance, aux_column_width, 1.0], [0.5, aux_column_width, 1.0], - [aux_column_width, seperator_distance, 1.0], - [seperator_distance, seperator_distance, 1.0], - [0.5, seperator_distance, 1.0], + [aux_column_width, separator_distance, 1.0], + [separator_distance, separator_distance, 1.0], + [0.5, separator_distance, 1.0], [aux_column_width, 0.5, 1.0], - [seperator_distance, 0.5, 1.0], + [separator_distance, 0.5, 1.0], [0.5, 0.5, 1.0], ] ) + _np.array([0.5, 0.5, 0.0]) @@ -484,14 +486,14 @@ def _closing_tile( branch_neighbor_x_min_ctps = _np.array( [ [-0.5, -aux_column_width, 0.0], - [-seperator_distance, -aux_column_width, 0.0], + [-separator_distance, -aux_column_width, 0.0], [-branch_thickness, -branch_thickness, 0.0], [-0.5, aux_column_width, 0.0], - [-seperator_distance, aux_column_width, 0.0], + [-separator_distance, aux_column_width, 0.0], [-branch_thickness, branch_thickness, 0.0], [-0.5, -aux_column_width, ctps_mid_height_bottom], [ - -seperator_distance, + -separator_distance, -aux_column_width, ctps_mid_height_bottom, ], @@ -502,7 +504,7 @@ def _closing_tile( ], [-0.5, aux_column_width, ctps_mid_height_bottom], [ - -seperator_distance, + -separator_distance, aux_column_width, ctps_mid_height_bottom, ], @@ -530,10 +532,10 @@ def _closing_tile( branch_neighbor_x_max_ctps = _np.array( [ [branch_thickness, -branch_thickness, 0.0], - [seperator_distance, -aux_column_width, 0.0], + [separator_distance, -aux_column_width, 0.0], [0.5, -aux_column_width, 0.0], [branch_thickness, branch_thickness, 0.0], - [seperator_distance, aux_column_width, 0.0], + [separator_distance, aux_column_width, 0.0], [0.5, aux_column_width, 0.0], [ branch_thickness, @@ -541,7 +543,7 @@ def _closing_tile( ctps_mid_height_bottom, ], [ - seperator_distance, + separator_distance, -aux_column_width, ctps_mid_height_bottom, ], @@ -552,7 +554,7 @@ def _closing_tile( ctps_mid_height_bottom, ], [ - seperator_distance, + separator_distance, aux_column_width, ctps_mid_height_bottom, ], @@ -577,20 +579,20 @@ def _closing_tile( [ [-aux_column_width, -0.5, 0.0], [aux_column_width, -0.5, 0.0], - [-aux_column_width, -seperator_distance, 0.0], - [aux_column_width, -seperator_distance, 0.0], + [-aux_column_width, -separator_distance, 0.0], + [aux_column_width, -separator_distance, 0.0], [-branch_thickness, -branch_thickness, 0.0], [branch_thickness, -branch_thickness, 0.0], [-aux_column_width, -0.5, ctps_mid_height_bottom], [aux_column_width, -0.5, ctps_mid_height_bottom], [ -aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_bottom, ], [ aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_bottom, ], [ @@ -623,8 +625,8 @@ def _closing_tile( [ [-branch_thickness, branch_thickness, 0.0], [branch_thickness, branch_thickness, 0.0], - [-aux_column_width, seperator_distance, 0.0], - [aux_column_width, seperator_distance, 0.0], + [-aux_column_width, separator_distance, 0.0], + [aux_column_width, separator_distance, 0.0], [-aux_column_width, 0.5, 0.0], [aux_column_width, 0.5, 0.0], [ @@ -639,12 +641,12 @@ def _closing_tile( ], [ -aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_bottom, ], [ aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_bottom, ], [-aux_column_width, 0.5, ctps_mid_height_bottom], @@ -668,31 +670,31 @@ def _closing_tile( branch_x_min_y_min_ctps = _np.array( [ [-0.5, -0.5, 0.0], - [-seperator_distance, -0.5, 0.0], + [-separator_distance, -0.5, 0.0], [-aux_column_width, -0.5, 0.0], - [-0.5, -seperator_distance, 0.0], - [-seperator_distance, -seperator_distance, 0.0], - [-aux_column_width, -seperator_distance, 0.0], + [-0.5, -separator_distance, 0.0], + [-separator_distance, -separator_distance, 0.0], + [-aux_column_width, -separator_distance, 0.0], [-0.5, -aux_column_width, 0.0], - [-seperator_distance, -aux_column_width, 0.0], + [-separator_distance, -aux_column_width, 0.0], [-branch_thickness, -branch_thickness, 0.0], [-0.5, -0.5, ctps_mid_height_bottom], - [-seperator_distance, -0.5, ctps_mid_height_bottom], + [-separator_distance, -0.5, ctps_mid_height_bottom], [-aux_column_width, -0.5, ctps_mid_height_bottom], - [-0.5, -seperator_distance, ctps_mid_height_bottom], + [-0.5, -separator_distance, ctps_mid_height_bottom], [ - -seperator_distance, - -seperator_distance, + -separator_distance, + -separator_distance, ctps_mid_height_bottom, ], [ -aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_bottom, ], [-0.5, -aux_column_width, ctps_mid_height_bottom], [ - -seperator_distance, + -separator_distance, -aux_column_width, ctps_mid_height_bottom, ], @@ -722,13 +724,13 @@ def _closing_tile( branch_x_max_y_max_ctps = _np.array( [ [branch_thickness, branch_thickness, 0.0], - [seperator_distance, aux_column_width, 0.0], + [separator_distance, aux_column_width, 0.0], [0.5, aux_column_width, 0.0], - [aux_column_width, seperator_distance, 0.0], - [seperator_distance, seperator_distance, 0.0], - [0.5, seperator_distance, 0.0], + [aux_column_width, separator_distance, 0.0], + [separator_distance, separator_distance, 0.0], + [0.5, separator_distance, 0.0], [aux_column_width, 0.5, 0.0], - [seperator_distance, 0.5, 0.0], + [separator_distance, 0.5, 0.0], [0.5, 0.5, 0.0], [ branch_thickness, @@ -736,24 +738,24 @@ def _closing_tile( ctps_mid_height_bottom, ], [ - seperator_distance, + separator_distance, aux_column_width, ctps_mid_height_bottom, ], [0.5, aux_column_width, ctps_mid_height_bottom], [ aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_bottom, ], [ - seperator_distance, - seperator_distance, + separator_distance, + separator_distance, ctps_mid_height_bottom, ], - [0.5, seperator_distance, ctps_mid_height_bottom], + [0.5, separator_distance, ctps_mid_height_bottom], [aux_column_width, 0.5, ctps_mid_height_bottom], - [seperator_distance, 0.5, ctps_mid_height_bottom], + [separator_distance, 0.5, ctps_mid_height_bottom], [0.5, 0.5, ctps_mid_height_bottom], [r_center, r_center, inv_filling_height], [half_r_center, r_center, inv_filling_height], @@ -776,35 +778,35 @@ def _closing_tile( branch_x_max_y_min_ctps = _np.array( [ [aux_column_width, -0.5, 0.0], - [seperator_distance, -0.5, 0.0], + [separator_distance, -0.5, 0.0], [0.5, -0.5, 0.0], - [aux_column_width, -seperator_distance, 0.0], - [seperator_distance, -seperator_distance, 0.0], - [0.5, -seperator_distance, 0.0], + [aux_column_width, -separator_distance, 0.0], + [separator_distance, -separator_distance, 0.0], + [0.5, -separator_distance, 0.0], [branch_thickness, -branch_thickness, 0.0], - [seperator_distance, -aux_column_width, 0.0], + [separator_distance, -aux_column_width, 0.0], [0.5, -aux_column_width, 0.0], [aux_column_width, -0.5, ctps_mid_height_bottom], - [seperator_distance, -0.5, ctps_mid_height_bottom], + [separator_distance, -0.5, ctps_mid_height_bottom], [0.5, -0.5, ctps_mid_height_bottom], [ aux_column_width, - -seperator_distance, + -separator_distance, ctps_mid_height_bottom, ], [ - seperator_distance, - -seperator_distance, + separator_distance, + -separator_distance, ctps_mid_height_bottom, ], - [0.5, -seperator_distance, ctps_mid_height_bottom], + [0.5, -separator_distance, ctps_mid_height_bottom], [ branch_thickness, -branch_thickness, ctps_mid_height_bottom, ], [ - seperator_distance, + separator_distance, -aux_column_width, ctps_mid_height_bottom, ], @@ -830,17 +832,17 @@ def _closing_tile( branch_x_min_y_max_ctps = _np.array( [ [-0.5, aux_column_width, 0.0], - [-seperator_distance, aux_column_width, 0.0], + [-separator_distance, aux_column_width, 0.0], [-branch_thickness, branch_thickness, 0.0], - [-0.5, seperator_distance, 0.0], - [-seperator_distance, seperator_distance, 0.0], - [-aux_column_width, seperator_distance, 0.0], + [-0.5, separator_distance, 0.0], + [-separator_distance, separator_distance, 0.0], + [-aux_column_width, separator_distance, 0.0], [-0.5, 0.5, 0.0], - [-seperator_distance, 0.5, 0.0], + [-separator_distance, 0.5, 0.0], [-aux_column_width, 0.5, 0.0], [-0.5, aux_column_width, ctps_mid_height_bottom], [ - -seperator_distance, + -separator_distance, aux_column_width, ctps_mid_height_bottom, ], @@ -849,19 +851,19 @@ def _closing_tile( branch_thickness, ctps_mid_height_bottom, ], - [-0.5, seperator_distance, ctps_mid_height_bottom], + [-0.5, separator_distance, ctps_mid_height_bottom], [ - -seperator_distance, - seperator_distance, + -separator_distance, + separator_distance, ctps_mid_height_bottom, ], [ -aux_column_width, - seperator_distance, + separator_distance, ctps_mid_height_bottom, ], [-0.5, 0.5, ctps_mid_height_bottom], - [-seperator_distance, 0.5, ctps_mid_height_bottom], + [-separator_distance, 0.5, ctps_mid_height_bottom], [-aux_column_width, 0.5, ctps_mid_height_bottom], [-0.5, r_center, inv_filling_height], [-half_r_center, r_center, inv_filling_height], @@ -889,7 +891,7 @@ def create_tile( self, parameters=None, parameter_sensitivities=None, - seperator_distance=None, + separator_distance=None, center_expansion=1.0, closure=None, **kwargs, # noqa ARG002 @@ -905,7 +907,7 @@ def create_tile( parameters : np.array only first entry is used, defines the internal radii of the branches - seperator_distance : float + separator_distance : float Control point distance to separation layer of biquadratic degrees, determines the minimum branch thickness (defaults to 0.4) center_expansion : float @@ -927,16 +929,16 @@ def create_tile( raise ValueError("Center Expansion must be in (.5, 1.5)") # Set default values - if seperator_distance is None: - seperator_distance = 0.3 + if separator_distance is None: + separator_distance = 0.3 if center_expansion is None: center_expansion = 1.0 # Check if all radii are in allowed range max_radius = min(0.5, (0.5 / center_expansion)) - max_radius = min(max_radius, seperator_distance) - min_radius = max(0.5 - seperator_distance, 0) + max_radius = min(max_radius, separator_distance) + min_radius = max(0.5 - separator_distance, 0) # set to default if nothing is given if parameters is None: @@ -967,7 +969,7 @@ def create_tile( return self._closing_tile( parameters=parameters, parameter_sensitivities=parameter_sensitivities, - seperator_distance=seperator_distance, + separator_distance=separator_distance, closure=closure, **kwargs, ) @@ -995,7 +997,7 @@ def create_tile( ] = _np.minimum(parameters.ravel(), center_r) # Branch midlength hd_center = 0.5 * (0.5 + center_r) - aux_column_width = 0.5 - 2 * (0.5 - seperator_distance) + aux_column_width = 0.5 - 2 * (0.5 - separator_distance) # Init return type spline_list = [] @@ -1004,18 +1006,18 @@ def create_tile( x_min_y_min = _np.array( [ [-0.5, -0.5, -aux_column_width], - [-seperator_distance, -0.5, -aux_column_width], + [-separator_distance, -0.5, -aux_column_width], [-y_min_r, -0.5, -y_min_r], - [-0.5, -seperator_distance, -aux_column_width], + [-0.5, -separator_distance, -aux_column_width], [-hd_center, -hd_center, -aux_column_width], [-aux_y_min, -hd_center, -aux_y_min], [-0.5, -x_min_r, -x_min_r], [-hd_center, -aux_x_min, -aux_x_min], [-center_r, -center_r, -center_r], [-0.5, -0.5, aux_column_width], - [-seperator_distance, -0.5, aux_column_width], + [-separator_distance, -0.5, aux_column_width], [-y_min_r, -0.5, y_min_r], - [-0.5, -seperator_distance, aux_column_width], + [-0.5, -separator_distance, aux_column_width], [-hd_center, -hd_center, aux_column_width], [-aux_y_min, -hd_center, aux_y_min], [-0.5, -x_min_r, x_min_r], @@ -1031,20 +1033,20 @@ def create_tile( x_max_y_min = _np.array( [ [y_min_r, -0.5, -y_min_r], - [seperator_distance, -0.5, -aux_column_width], + [separator_distance, -0.5, -aux_column_width], [0.5, -0.5, -aux_column_width], [aux_y_min, -hd_center, -aux_y_min], [hd_center, -hd_center, -aux_column_width], - [0.5, -seperator_distance, -aux_column_width], + [0.5, -separator_distance, -aux_column_width], [center_r, -center_r, -center_r], [hd_center, -aux_x_max, -aux_x_max], [0.5, -x_max_r, -x_max_r], [y_min_r, -0.5, y_min_r], - [seperator_distance, -0.5, aux_column_width], + [separator_distance, -0.5, aux_column_width], [0.5, -0.5, aux_column_width], [aux_y_min, -hd_center, aux_y_min], [hd_center, -hd_center, aux_column_width], - [0.5, -seperator_distance, aux_column_width], + [0.5, -separator_distance, aux_column_width], [center_r, -center_r, center_r], [hd_center, -aux_x_max, aux_x_max], [0.5, -x_max_r, x_max_r], @@ -1060,20 +1062,20 @@ def create_tile( [-0.5, x_min_r, -x_min_r], [-hd_center, aux_x_min, -aux_x_min], [-center_r, center_r, -center_r], - [-0.5, seperator_distance, -aux_column_width], + [-0.5, separator_distance, -aux_column_width], [-hd_center, hd_center, -aux_column_width], [-aux_y_max, hd_center, -aux_y_max], [-0.5, 0.5, -aux_column_width], - [-seperator_distance, 0.5, -aux_column_width], + [-separator_distance, 0.5, -aux_column_width], [-y_max_r, 0.5, -y_max_r], [-0.5, x_min_r, x_min_r], [-hd_center, aux_x_min, aux_x_min], [-center_r, center_r, center_r], - [-0.5, seperator_distance, aux_column_width], + [-0.5, separator_distance, aux_column_width], [-hd_center, hd_center, aux_column_width], [-aux_y_max, hd_center, aux_y_max], [-0.5, 0.5, aux_column_width], - [-seperator_distance, 0.5, aux_column_width], + [-separator_distance, 0.5, aux_column_width], [-y_max_r, 0.5, y_max_r], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1089,18 +1091,18 @@ def create_tile( [0.5, x_max_r, -x_max_r], [aux_y_max, hd_center, -aux_y_max], [hd_center, hd_center, -aux_column_width], - [0.5, seperator_distance, -aux_column_width], + [0.5, separator_distance, -aux_column_width], [y_max_r, 0.5, -y_max_r], - [seperator_distance, 0.5, -aux_column_width], + [separator_distance, 0.5, -aux_column_width], [0.5, 0.5, -aux_column_width], [center_r, center_r, center_r], [hd_center, aux_x_max, aux_x_max], [0.5, x_max_r, x_max_r], [aux_y_max, hd_center, aux_y_max], [hd_center, hd_center, aux_column_width], - [0.5, seperator_distance, aux_column_width], + [0.5, separator_distance, aux_column_width], [y_max_r, 0.5, y_max_r], - [seperator_distance, 0.5, aux_column_width], + [separator_distance, 0.5, aux_column_width], [0.5, 0.5, aux_column_width], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1112,15 +1114,15 @@ def create_tile( x_min_z_min = _np.array( [ [-0.5, -aux_column_width, -0.5], - [-seperator_distance, -aux_column_width, -0.5], + [-separator_distance, -aux_column_width, -0.5], [-z_min_r, -z_min_r, -0.5], [-0.5, aux_column_width, -0.5], - [-seperator_distance, aux_column_width, -0.5], + [-separator_distance, aux_column_width, -0.5], [-z_min_r, z_min_r, -0.5], - [-0.5, -aux_column_width, -seperator_distance], + [-0.5, -aux_column_width, -separator_distance], [-hd_center, -aux_column_width, -hd_center], [-aux_z_min, -aux_z_min, -hd_center], - [-0.5, aux_column_width, -seperator_distance], + [-0.5, aux_column_width, -separator_distance], [-hd_center, aux_column_width, -hd_center], [-aux_z_min, aux_z_min, -hd_center], [-0.5, -x_min_r, -x_min_r], @@ -1139,17 +1141,17 @@ def create_tile( x_max_z_min = _np.array( [ [z_min_r, -z_min_r, -0.5], - [seperator_distance, -aux_column_width, -0.5], + [separator_distance, -aux_column_width, -0.5], [0.5, -aux_column_width, -0.5], [z_min_r, z_min_r, -0.5], - [seperator_distance, aux_column_width, -0.5], + [separator_distance, aux_column_width, -0.5], [0.5, aux_column_width, -0.5], [aux_z_min, -aux_z_min, -hd_center], [hd_center, -aux_column_width, -hd_center], - [0.5, -aux_column_width, -seperator_distance], + [0.5, -aux_column_width, -separator_distance], [aux_z_min, aux_z_min, -hd_center], [hd_center, aux_column_width, -hd_center], - [0.5, aux_column_width, -seperator_distance], + [0.5, aux_column_width, -separator_distance], [center_r, -center_r, -center_r], [hd_center, -aux_x_max, -aux_x_max], [0.5, -x_max_r, -x_max_r], @@ -1171,17 +1173,17 @@ def create_tile( [-0.5, x_min_r, x_min_r], [-hd_center, aux_x_min, aux_x_min], [-center_r, center_r, center_r], - [-0.5, -aux_column_width, seperator_distance], + [-0.5, -aux_column_width, separator_distance], [-hd_center, -aux_column_width, hd_center], [-aux_z_max, -aux_z_max, hd_center], - [-0.5, aux_column_width, seperator_distance], + [-0.5, aux_column_width, separator_distance], [-hd_center, aux_column_width, hd_center], [-aux_z_max, aux_z_max, hd_center], [-0.5, -aux_column_width, 0.5], - [-seperator_distance, -aux_column_width, 0.5], + [-separator_distance, -aux_column_width, 0.5], [-z_max_r, -z_max_r, 0.5], [-0.5, aux_column_width, 0.5], - [-seperator_distance, aux_column_width, 0.5], + [-separator_distance, aux_column_width, 0.5], [-z_max_r, z_max_r, 0.5], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1200,15 +1202,15 @@ def create_tile( [0.5, x_max_r, x_max_r], [aux_z_max, -aux_z_max, hd_center], [hd_center, -aux_column_width, hd_center], - [0.5, -aux_column_width, seperator_distance], + [0.5, -aux_column_width, separator_distance], [aux_z_max, aux_z_max, hd_center], [hd_center, aux_column_width, hd_center], - [0.5, aux_column_width, seperator_distance], + [0.5, aux_column_width, separator_distance], [z_max_r, -z_max_r, 0.5], - [seperator_distance, -aux_column_width, 0.5], + [separator_distance, -aux_column_width, 0.5], [0.5, -aux_column_width, 0.5], [z_max_r, z_max_r, 0.5], - [seperator_distance, aux_column_width, 0.5], + [separator_distance, aux_column_width, 0.5], [0.5, aux_column_width, 0.5], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1221,12 +1223,12 @@ def create_tile( [ [-aux_column_width, -0.5, -0.5], [aux_column_width, -0.5, -0.5], - [-aux_column_width, -seperator_distance, -0.5], - [aux_column_width, -seperator_distance, -0.5], + [-aux_column_width, -separator_distance, -0.5], + [aux_column_width, -separator_distance, -0.5], [-z_min_r, -z_min_r, -0.5], [z_min_r, -z_min_r, -0.5], - [-aux_column_width, -0.5, -seperator_distance], - [aux_column_width, -0.5, -seperator_distance], + [-aux_column_width, -0.5, -separator_distance], + [aux_column_width, -0.5, -separator_distance], [-aux_column_width, -hd_center, -hd_center], [aux_column_width, -hd_center, -hd_center], [-aux_z_min, -aux_z_min, -hd_center], @@ -1248,16 +1250,16 @@ def create_tile( [ [-z_min_r, z_min_r, -0.5], [z_min_r, z_min_r, -0.5], - [-aux_column_width, seperator_distance, -0.5], - [aux_column_width, seperator_distance, -0.5], + [-aux_column_width, separator_distance, -0.5], + [aux_column_width, separator_distance, -0.5], [-aux_column_width, 0.5, -0.5], [aux_column_width, 0.5, -0.5], [-aux_z_min, aux_z_min, -hd_center], [aux_z_min, aux_z_min, -hd_center], [-aux_column_width, hd_center, -hd_center], [aux_column_width, hd_center, -hd_center], - [-aux_column_width, 0.5, -seperator_distance], - [aux_column_width, 0.5, -seperator_distance], + [-aux_column_width, 0.5, -separator_distance], + [aux_column_width, 0.5, -separator_distance], [-center_r, center_r, -center_r], [center_r, center_r, -center_r], [-aux_y_max, hd_center, -aux_y_max], @@ -1279,16 +1281,16 @@ def create_tile( [aux_y_min, -hd_center, aux_y_min], [-center_r, -center_r, center_r], [center_r, -center_r, center_r], - [-aux_column_width, -0.5, seperator_distance], - [aux_column_width, -0.5, seperator_distance], + [-aux_column_width, -0.5, separator_distance], + [aux_column_width, -0.5, separator_distance], [-aux_column_width, -hd_center, hd_center], [aux_column_width, -hd_center, hd_center], [-aux_z_max, -aux_z_max, hd_center], [aux_z_max, -aux_z_max, hd_center], [-aux_column_width, -0.5, 0.5], [aux_column_width, -0.5, 0.5], - [-aux_column_width, -seperator_distance, 0.5], - [aux_column_width, -seperator_distance, 0.5], + [-aux_column_width, -separator_distance, 0.5], + [aux_column_width, -separator_distance, 0.5], [-z_max_r, -z_max_r, 0.5], [z_max_r, -z_max_r, 0.5], ] @@ -1310,12 +1312,12 @@ def create_tile( [aux_z_max, aux_z_max, hd_center], [-aux_column_width, hd_center, hd_center], [aux_column_width, hd_center, hd_center], - [-aux_column_width, 0.5, seperator_distance], - [aux_column_width, 0.5, seperator_distance], + [-aux_column_width, 0.5, separator_distance], + [aux_column_width, 0.5, separator_distance], [-z_max_r, z_max_r, 0.5], [z_max_r, z_max_r, 0.5], - [-aux_column_width, seperator_distance, 0.5], - [aux_column_width, seperator_distance, 0.5], + [-aux_column_width, separator_distance, 0.5], + [aux_column_width, separator_distance, 0.5], [-aux_column_width, 0.5, 0.5], [aux_column_width, 0.5, 0.5], ] @@ -1328,27 +1330,27 @@ def create_tile( x_min_y_min_z_min = _np.array( [ [-0.5, -0.5, -0.5], - [-seperator_distance, -0.5, -0.5], + [-separator_distance, -0.5, -0.5], [-aux_column_width, -0.5, -0.5], - [-0.5, -seperator_distance, -0.5], - [-seperator_distance, -seperator_distance, -0.5], - [-aux_column_width, -seperator_distance, -0.5], + [-0.5, -separator_distance, -0.5], + [-separator_distance, -separator_distance, -0.5], + [-aux_column_width, -separator_distance, -0.5], [-0.5, -aux_column_width, -0.5], - [-seperator_distance, -aux_column_width, -0.5], + [-separator_distance, -aux_column_width, -0.5], [-z_min_r, -z_min_r, -0.5], - [-0.5, -0.5, -seperator_distance], - [-seperator_distance, -0.5, -seperator_distance], - [-aux_column_width, -0.5, -seperator_distance], - [-0.5, -seperator_distance, -seperator_distance], + [-0.5, -0.5, -separator_distance], + [-separator_distance, -0.5, -separator_distance], + [-aux_column_width, -0.5, -separator_distance], + [-0.5, -separator_distance, -separator_distance], [-hd_center, -hd_center, -hd_center], [-aux_column_width, -hd_center, -hd_center], - [-0.5, -aux_column_width, -seperator_distance], + [-0.5, -aux_column_width, -separator_distance], [-hd_center, -aux_column_width, -hd_center], [-aux_z_min, -aux_z_min, -hd_center], [-0.5, -0.5, -aux_column_width], - [-seperator_distance, -0.5, -aux_column_width], + [-separator_distance, -0.5, -aux_column_width], [-y_min_r, -0.5, -y_min_r], - [-0.5, -seperator_distance, -aux_column_width], + [-0.5, -separator_distance, -aux_column_width], [-hd_center, -hd_center, -aux_column_width], [-aux_y_min, -hd_center, -aux_y_min], [-0.5, -x_min_r, -x_min_r], @@ -1364,29 +1366,29 @@ def create_tile( x_max_y_min_z_min = _np.array( [ [aux_column_width, -0.5, -0.5], - [seperator_distance, -0.5, -0.5], + [separator_distance, -0.5, -0.5], [0.5, -0.5, -0.5], - [aux_column_width, -seperator_distance, -0.5], - [seperator_distance, -seperator_distance, -0.5], - [0.5, -seperator_distance, -0.5], + [aux_column_width, -separator_distance, -0.5], + [separator_distance, -separator_distance, -0.5], + [0.5, -separator_distance, -0.5], [z_min_r, -z_min_r, -0.5], - [seperator_distance, -aux_column_width, -0.5], + [separator_distance, -aux_column_width, -0.5], [0.5, -aux_column_width, -0.5], - [aux_column_width, -0.5, -seperator_distance], - [seperator_distance, -0.5, -seperator_distance], - [0.5, -0.5, -seperator_distance], + [aux_column_width, -0.5, -separator_distance], + [separator_distance, -0.5, -separator_distance], + [0.5, -0.5, -separator_distance], [aux_column_width, -hd_center, -hd_center], [hd_center, -hd_center, -hd_center], - [0.5, -seperator_distance, -seperator_distance], + [0.5, -separator_distance, -separator_distance], [aux_z_min, -aux_z_min, -hd_center], [hd_center, -aux_column_width, -hd_center], - [0.5, -aux_column_width, -seperator_distance], + [0.5, -aux_column_width, -separator_distance], [y_min_r, -0.5, -y_min_r], - [seperator_distance, -0.5, -aux_column_width], + [separator_distance, -0.5, -aux_column_width], [0.5, -0.5, -aux_column_width], [aux_y_min, -hd_center, -aux_y_min], [hd_center, -hd_center, -aux_column_width], - [0.5, -seperator_distance, -aux_column_width], + [0.5, -separator_distance, -aux_column_width], [center_r, -center_r, -center_r], [hd_center, -aux_x_max, -aux_x_max], [0.5, -x_max_r, -x_max_r], @@ -1400,31 +1402,31 @@ def create_tile( x_min_y_max_z_min = _np.array( [ [-0.5, aux_column_width, -0.5], - [-seperator_distance, aux_column_width, -0.5], + [-separator_distance, aux_column_width, -0.5], [-z_min_r, z_min_r, -0.5], - [-0.5, seperator_distance, -0.5], - [-seperator_distance, seperator_distance, -0.5], - [-aux_column_width, seperator_distance, -0.5], + [-0.5, separator_distance, -0.5], + [-separator_distance, separator_distance, -0.5], + [-aux_column_width, separator_distance, -0.5], [-0.5, 0.5, -0.5], - [-seperator_distance, 0.5, -0.5], + [-separator_distance, 0.5, -0.5], [-aux_column_width, 0.5, -0.5], - [-0.5, aux_column_width, -seperator_distance], + [-0.5, aux_column_width, -separator_distance], [-hd_center, aux_column_width, -hd_center], [-aux_z_min, aux_z_min, -hd_center], - [-0.5, seperator_distance, -seperator_distance], + [-0.5, separator_distance, -separator_distance], [-hd_center, hd_center, -hd_center], [-aux_column_width, hd_center, -hd_center], - [-0.5, 0.5, -seperator_distance], - [-seperator_distance, 0.5, -seperator_distance], - [-aux_column_width, 0.5, -seperator_distance], + [-0.5, 0.5, -separator_distance], + [-separator_distance, 0.5, -separator_distance], + [-aux_column_width, 0.5, -separator_distance], [-0.5, x_min_r, -x_min_r], [-hd_center, aux_x_min, -aux_x_min], [-center_r, center_r, -center_r], - [-0.5, seperator_distance, -aux_column_width], + [-0.5, separator_distance, -aux_column_width], [-hd_center, hd_center, -aux_column_width], [-aux_y_max, hd_center, -aux_y_max], [-0.5, 0.5, -aux_column_width], - [-seperator_distance, 0.5, -aux_column_width], + [-separator_distance, 0.5, -aux_column_width], [-y_max_r, 0.5, -y_max_r], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1436,31 +1438,31 @@ def create_tile( x_max_y_max_z_min = _np.array( [ [z_min_r, z_min_r, -0.5], - [seperator_distance, aux_column_width, -0.5], + [separator_distance, aux_column_width, -0.5], [0.5, aux_column_width, -0.5], - [aux_column_width, seperator_distance, -0.5], - [seperator_distance, seperator_distance, -0.5], - [0.5, seperator_distance, -0.5], + [aux_column_width, separator_distance, -0.5], + [separator_distance, separator_distance, -0.5], + [0.5, separator_distance, -0.5], [aux_column_width, 0.5, -0.5], - [seperator_distance, 0.5, -0.5], + [separator_distance, 0.5, -0.5], [0.5, 0.5, -0.5], [aux_z_min, aux_z_min, -hd_center], [hd_center, aux_column_width, -hd_center], - [0.5, aux_column_width, -seperator_distance], + [0.5, aux_column_width, -separator_distance], [aux_column_width, hd_center, -hd_center], [hd_center, hd_center, -hd_center], - [0.5, seperator_distance, -seperator_distance], - [aux_column_width, 0.5, -seperator_distance], - [seperator_distance, 0.5, -seperator_distance], - [0.5, 0.5, -seperator_distance], + [0.5, separator_distance, -separator_distance], + [aux_column_width, 0.5, -separator_distance], + [separator_distance, 0.5, -separator_distance], + [0.5, 0.5, -separator_distance], [center_r, center_r, -center_r], [hd_center, aux_x_max, -aux_x_max], [0.5, x_max_r, -x_max_r], [aux_y_max, hd_center, -aux_y_max], [hd_center, hd_center, -aux_column_width], - [0.5, seperator_distance, -aux_column_width], + [0.5, separator_distance, -aux_column_width], [y_max_r, 0.5, -y_max_r], - [seperator_distance, 0.5, -aux_column_width], + [separator_distance, 0.5, -aux_column_width], [0.5, 0.5, -aux_column_width], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1472,31 +1474,31 @@ def create_tile( x_min_y_min_z_max = _np.array( [ [-0.5, -0.5, aux_column_width], - [-seperator_distance, -0.5, aux_column_width], + [-separator_distance, -0.5, aux_column_width], [-y_min_r, -0.5, y_min_r], - [-0.5, -seperator_distance, aux_column_width], + [-0.5, -separator_distance, aux_column_width], [-hd_center, -hd_center, aux_column_width], [-aux_y_min, -hd_center, aux_y_min], [-0.5, -x_min_r, x_min_r], [-hd_center, -aux_x_min, aux_x_min], [-center_r, -center_r, center_r], - [-0.5, -0.5, seperator_distance], - [-seperator_distance, -0.5, seperator_distance], - [-aux_column_width, -0.5, seperator_distance], - [-0.5, -seperator_distance, seperator_distance], + [-0.5, -0.5, separator_distance], + [-separator_distance, -0.5, separator_distance], + [-aux_column_width, -0.5, separator_distance], + [-0.5, -separator_distance, separator_distance], [-hd_center, -hd_center, hd_center], [-aux_column_width, -hd_center, hd_center], - [-0.5, -aux_column_width, seperator_distance], + [-0.5, -aux_column_width, separator_distance], [-hd_center, -aux_column_width, hd_center], [-aux_z_max, -aux_z_max, hd_center], [-0.5, -0.5, 0.5], - [-seperator_distance, -0.5, 0.5], + [-separator_distance, -0.5, 0.5], [-aux_column_width, -0.5, 0.5], - [-0.5, -seperator_distance, 0.5], - [-seperator_distance, -seperator_distance, 0.5], - [-aux_column_width, -seperator_distance, 0.5], + [-0.5, -separator_distance, 0.5], + [-separator_distance, -separator_distance, 0.5], + [-aux_column_width, -separator_distance, 0.5], [-0.5, -aux_column_width, 0.5], - [-seperator_distance, -aux_column_width, 0.5], + [-separator_distance, -aux_column_width, 0.5], [-z_max_r, -z_max_r, 0.5], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1508,31 +1510,31 @@ def create_tile( x_max_y_min_z_max = _np.array( [ [y_min_r, -0.5, y_min_r], - [seperator_distance, -0.5, aux_column_width], + [separator_distance, -0.5, aux_column_width], [0.5, -0.5, aux_column_width], [aux_y_min, -hd_center, aux_y_min], [hd_center, -hd_center, aux_column_width], - [0.5, -seperator_distance, aux_column_width], + [0.5, -separator_distance, aux_column_width], [center_r, -center_r, center_r], [hd_center, -aux_x_max, aux_x_max], [0.5, -x_max_r, x_max_r], - [aux_column_width, -0.5, seperator_distance], - [seperator_distance, -0.5, seperator_distance], - [0.5, -0.5, seperator_distance], + [aux_column_width, -0.5, separator_distance], + [separator_distance, -0.5, separator_distance], + [0.5, -0.5, separator_distance], [aux_column_width, -hd_center, hd_center], [hd_center, -hd_center, hd_center], - [0.5, -seperator_distance, seperator_distance], + [0.5, -separator_distance, separator_distance], [aux_z_max, -aux_z_max, hd_center], [hd_center, -aux_column_width, hd_center], - [0.5, -aux_column_width, seperator_distance], + [0.5, -aux_column_width, separator_distance], [aux_column_width, -0.5, 0.5], - [seperator_distance, -0.5, 0.5], + [separator_distance, -0.5, 0.5], [0.5, -0.5, 0.5], - [aux_column_width, -seperator_distance, 0.5], - [seperator_distance, -seperator_distance, 0.5], - [0.5, -seperator_distance, 0.5], + [aux_column_width, -separator_distance, 0.5], + [separator_distance, -separator_distance, 0.5], + [0.5, -separator_distance, 0.5], [z_max_r, -z_max_r, 0.5], - [seperator_distance, -aux_column_width, 0.5], + [separator_distance, -aux_column_width, 0.5], [0.5, -aux_column_width, 0.5], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1546,29 +1548,29 @@ def create_tile( [-0.5, x_min_r, x_min_r], [-hd_center, aux_x_min, aux_x_min], [-center_r, center_r, center_r], - [-0.5, seperator_distance, aux_column_width], + [-0.5, separator_distance, aux_column_width], [-hd_center, hd_center, aux_column_width], [-aux_y_max, hd_center, aux_y_max], [-0.5, 0.5, aux_column_width], - [-seperator_distance, 0.5, aux_column_width], + [-separator_distance, 0.5, aux_column_width], [-y_max_r, 0.5, y_max_r], - [-0.5, aux_column_width, seperator_distance], + [-0.5, aux_column_width, separator_distance], [-hd_center, aux_column_width, hd_center], [-aux_z_max, aux_z_max, hd_center], - [-0.5, seperator_distance, seperator_distance], + [-0.5, separator_distance, separator_distance], [-hd_center, hd_center, hd_center], [-aux_column_width, hd_center, hd_center], - [-0.5, 0.5, seperator_distance], - [-seperator_distance, 0.5, seperator_distance], - [-aux_column_width, 0.5, seperator_distance], + [-0.5, 0.5, separator_distance], + [-separator_distance, 0.5, separator_distance], + [-aux_column_width, 0.5, separator_distance], [-0.5, aux_column_width, 0.5], - [-seperator_distance, aux_column_width, 0.5], + [-separator_distance, aux_column_width, 0.5], [-z_max_r, z_max_r, 0.5], - [-0.5, seperator_distance, 0.5], - [-seperator_distance, seperator_distance, 0.5], - [-aux_column_width, seperator_distance, 0.5], + [-0.5, separator_distance, 0.5], + [-separator_distance, separator_distance, 0.5], + [-aux_column_width, separator_distance, 0.5], [-0.5, 0.5, 0.5], - [-seperator_distance, 0.5, 0.5], + [-separator_distance, 0.5, 0.5], [-aux_column_width, 0.5, 0.5], ] ) + _np.array([0.5, 0.5, 0.5]) @@ -1584,27 +1586,27 @@ def create_tile( [0.5, x_max_r, x_max_r], [aux_y_max, hd_center, aux_y_max], [hd_center, hd_center, aux_column_width], - [0.5, seperator_distance, aux_column_width], + [0.5, separator_distance, aux_column_width], [y_max_r, 0.5, y_max_r], - [seperator_distance, 0.5, aux_column_width], + [separator_distance, 0.5, aux_column_width], [0.5, 0.5, aux_column_width], [aux_z_max, aux_z_max, hd_center], [hd_center, aux_column_width, hd_center], - [0.5, aux_column_width, seperator_distance], + [0.5, aux_column_width, separator_distance], [aux_column_width, hd_center, hd_center], [hd_center, hd_center, hd_center], - [0.5, seperator_distance, seperator_distance], - [aux_column_width, 0.5, seperator_distance], - [seperator_distance, 0.5, seperator_distance], - [0.5, 0.5, seperator_distance], + [0.5, separator_distance, separator_distance], + [aux_column_width, 0.5, separator_distance], + [separator_distance, 0.5, separator_distance], + [0.5, 0.5, separator_distance], [z_max_r, z_max_r, 0.5], - [seperator_distance, aux_column_width, 0.5], + [separator_distance, aux_column_width, 0.5], [0.5, aux_column_width, 0.5], - [aux_column_width, seperator_distance, 0.5], - [seperator_distance, seperator_distance, 0.5], - [0.5, seperator_distance, 0.5], + [aux_column_width, separator_distance, 0.5], + [separator_distance, separator_distance, 0.5], + [0.5, separator_distance, 0.5], [aux_column_width, 0.5, 0.5], - [seperator_distance, 0.5, 0.5], + [separator_distance, 0.5, 0.5], [0.5, 0.5, 0.5], ] ) + _np.array([0.5, 0.5, 0.5]) diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index dac00bd20..7df89d6e8 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -20,6 +20,8 @@ class Snappy(_TileBase): # dummy values - not used _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 1 + _sensitivities_implemented = False + _closure_directions = ["y_min", "y_max"] def _closing_tile( self, @@ -362,7 +364,7 @@ def create_tile( if parameters is not None: raise NotImplementedError( - "Parametriazation is not implemented for this tile yet" + "Parametrization is not implemented for this tile yet" ) if parameter_sensitivities is not None: diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py new file mode 100644 index 000000000..cfb59555e --- /dev/null +++ b/tests/test_microstructure.py @@ -0,0 +1,75 @@ +from inspect import getfullargspec + +import numpy as np + +import splinepy.microstructure as ms + +EPS = 1e-8 + +all_tile_classes = list(ms.tiles.everything().values()) + + +def test_tile_class(): + """Checks if all tile classes have the appropriate members and functions.""" + required_class_variables = { + "_para_dim": int, + "_dim": int, + "_evaluation_points": np.ndarray, + "_n_info_per_eval_point": int, + # "_sensitivities_implemented": bool + } + + # Go through all available tiles + for tile_class in all_tile_classes: + # Get tile class' objects + members = [ + attr for attr in dir(tile_class) if not attr.startswith("__") + ] + + # Class must have function create_tile() + assert hasattr( + tile_class, "create_tile" + ), "Tile class must have create_tile()" + # Tile must be able to take parameters and sensitivities as input + create_parameters = getfullargspec(tile_class.create_tile).args + for required_param in ["parameters", "parameter_sensitivities"]: + assert ( + required_param in create_parameters + ), f"create_tile() must have '{required_param}' as an input parameter" + + # Ensure closure can be correctly handled + if "closure" in create_parameters: + assert "_closure_directions" in members, ( + "Tile class has closure ability. The available closure directions " + + "are missing" + ) + assert hasattr( + tile_class, "_closing_tile" + ), "Tile class has closure ability but no _closing_tile() function!" + + # Check if tile class has all required variables and they are the correct type + for required_variable, var_type in required_class_variables.items(): + assert ( + required_variable in members + ), f"Tile class needs to have member variable '{required_variable}'" + assert isinstance( + eval(f"tile_class.{required_variable}"), var_type + ), f"Variable {required_variable} needs to be of type {var_type}" + + +def test_tile_bounds(): + """Test if tile is still in unit cube at the bounds""" + # TODO + pass + + +def test_tile_derivatives(): + """Testing the correctness of the tile derivatives""" + # TODO + pass + + +def test_tile_closure(): + """Check if closing tiles also lie in unit cube. Maybe also check derivatives.""" + # TODO + pass From 617a6cc36a6ec237045f9f2520dfb40889723cad Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 9 Aug 2024 16:29:50 +0200 Subject: [PATCH 077/171] WIP add test if all microtiles lie in unit cube (only for default parameters) --- tests/test_microstructure.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index cfb59555e..6e3cd8330 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -16,7 +16,7 @@ def test_tile_class(): "_dim": int, "_evaluation_points": np.ndarray, "_n_info_per_eval_point": int, - # "_sensitivities_implemented": bool + "_sensitivities_implemented": bool, } # Go through all available tiles @@ -58,13 +58,28 @@ def test_tile_class(): def test_tile_bounds(): - """Test if tile is still in unit cube at the bounds""" - # TODO - pass + """Test if tile is still in unit cube at the bounds. Currently only checks + default parameter values.""" + # TODO: also check non-default parameters + + def check_control_points(tile_patches): + """Check if all of tile's control points all lie within unit square/cube""" + # Go through all patches + for tile_patch in tile_patches: + cps = tile_patch.control_points + assert np.all( + (cps >= 0.0) & (cps <= 1.0) + ), "Control points of tile must lie inside the unit square/cube" + + for tile_class in all_tile_classes: + tile_creator = tile_class() + # Create tile with default parameters + tile_patches, _ = tile_creator.create_tile() + check_control_points(tile_patches) def test_tile_derivatives(): - """Testing the correctness of the tile derivatives""" + """Testing the correctness of the tile derivatives using Finite Differences""" # TODO pass From 89fcdbf7b7e862b9b4e48093eab9acc68bf9ccca Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 12 Aug 2024 13:31:55 +0200 Subject: [PATCH 078/171] Add test for microstructure closures --- splinepy/microstructure/tiles/armadillo.py | 1 + splinepy/microstructure/tiles/chi.py | 2 +- splinepy/microstructure/tiles/cross_2d.py | 15 ++++------ splinepy/microstructure/tiles/cross_3d.py | 3 ++ .../microstructure/tiles/cross_3d_linear.py | 3 ++ splinepy/microstructure/tiles/snappy.py | 8 ++++-- tests/test_microstructure.py | 28 +++++++++++++++++-- 7 files changed, 43 insertions(+), 17 deletions(-) diff --git a/splinepy/microstructure/tiles/armadillo.py b/splinepy/microstructure/tiles/armadillo.py index 9bbf602da..acf8828be 100644 --- a/splinepy/microstructure/tiles/armadillo.py +++ b/splinepy/microstructure/tiles/armadillo.py @@ -29,6 +29,7 @@ class Armadillo(_TileBase): "z_min", "z_max", ] + _parameter_bounds = [[0.0, 0.5]] def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/chi.py b/splinepy/microstructure/tiles/chi.py index e3cbf708b..707484c38 100644 --- a/splinepy/microstructure/tiles/chi.py +++ b/splinepy/microstructure/tiles/chi.py @@ -20,7 +20,7 @@ class Chi(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = True - _parameter_bounds = {"parameters": [-_np.pi / 2, _np.pi / 2]} + _parameter_bounds = [[-_np.pi / 2, _np.pi / 2]] def create_tile( self, diff --git a/splinepy/microstructure/tiles/cross_2d.py b/splinepy/microstructure/tiles/cross_2d.py index cf4a988d2..147da3cee 100644 --- a/splinepy/microstructure/tiles/cross_2d.py +++ b/splinepy/microstructure/tiles/cross_2d.py @@ -26,9 +26,11 @@ class Cross2D(_TileBase): ] ) _n_info_per_eval_point = 1 - _parameter_bounds = {"center_expansion": [0.5, 1.5]} _sensitivities_implemented = True _closure_directions = ["x_min", "x_max", "y_min", "y_max"] + _parameter_bounds = [ + [0.0, 0.5] + ] * 4 # valid for default center_expansion=1.0 def _closing_tile( self, @@ -420,15 +422,8 @@ def create_tile( if not isinstance(center_expansion, float): raise ValueError("Invalid Type") - center_expansion_bounds = self._parameter_bounds["center_expansion"] - if not ( - (center_expansion > center_expansion_bounds[0]) - and (center_expansion < center_expansion_bounds[1]) - ): - error_message = ( - "Center Expansion must be in (" - + f"{center_expansion_bounds[0]}, {center_expansion_bounds[1]})" - ) + if not ((center_expansion > 0.5) and (center_expansion < 1.5)): + error_message = "Center Expansion must be in (0.5, 1.5)" raise ValueError(error_message) max_radius = min(0.5, (0.5 / center_expansion)) diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index 01282c6f3..435ac3811 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -30,6 +30,9 @@ class Cross3D(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = True _closure_directions = ["z_min", "z_max"] + _parameter_bounds = [ + [0.0, 0.5] + ] * 6 # valid for default center_expansion=1.0 def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index 952738afe..535592aa4 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -30,6 +30,9 @@ class Cross3DLinear(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = True _closure_directions = ["z_min", "z_max"] + _parameter_bounds = [ + [0.0, 0.5] + ] * 6 # valid for default center_expansion=1.0 def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index 7df89d6e8..60cab586f 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -63,7 +63,9 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - self.check_params(parameters) + # TODO: parameters are not implemented, therefore do not check params + if parameters is not None: + self.check_params(parameters) if parameter_sensitivities is not None: raise NotImplementedError( @@ -222,7 +224,6 @@ def _closing_tile( spline_list.append( _Bezier(degrees=[3, 1], control_points=spline_10) ) - return spline_list elif closure == "y_max": spline_1 = _np.array( [ @@ -288,12 +289,13 @@ def _closing_tile( spline_list.append( _Bezier(degrees=[3, 1], control_points=spline_5) ) - return (spline_list, None) else: raise ValueError( "Closing tile is only implemented for y-enclosure" ) + return (spline_list, None) + def create_tile( self, parameters=None, diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 6e3cd8330..b287b5b09 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -81,10 +81,32 @@ def check_control_points(tile_patches): def test_tile_derivatives(): """Testing the correctness of the tile derivatives using Finite Differences""" # TODO - pass + for tile_class in all_tile_classes: + tile_creator = tile_class() + # Skip test if tile class has no implemented sensitivities + if not tile_creator._sensitivities_implemented: + continue + pass def test_tile_closure(): """Check if closing tiles also lie in unit cube. Maybe also check derivatives.""" - # TODO - pass + # TODO: check closure also for extreme values of parameters + for tile_class in all_tile_classes: + # Skip tile if if does not support closure + if "_closure_directions" not in dir(tile_class): + continue + tile_creator = tile_class() + # Go through all implemented closure directions + for closure_direction in tile_creator._closure_directions: + tile_patches, sensitivities = tile_creator.create_tile( + closure=closure_direction + ) + assert ( + sensitivities is None + ), "Ensure sensitivities for closure are None if no sensitivities are asked" + for tile_patch in tile_patches: + cps = tile_patch.control_points + assert np.all( + (cps >= 0.0) & (cps <= 1.0) + ), "Control points of tile must lie inside the unit square/cube" From cb29a7573805cfcd6e8fdc6b14ad0be7399842d5 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 12 Aug 2024 15:54:45 +0200 Subject: [PATCH 079/171] Add test for non-default parameters of microtiles --- splinepy/microstructure/tiles/armadillo.py | 1 + splinepy/microstructure/tiles/chi.py | 4 +- splinepy/microstructure/tiles/cross_2d.py | 1 + splinepy/microstructure/tiles/cross_3d.py | 1 + .../microstructure/tiles/cross_3d_linear.py | 1 + splinepy/microstructure/tiles/cube_void.py | 13 +++- .../microstructure/tiles/double_lattice.py | 2 + splinepy/microstructure/tiles/ellips_v_oid.py | 10 +++ splinepy/microstructure/tiles/hollow_cube.py | 2 + .../microstructure/tiles/hollow_octagon.py | 2 + .../tiles/hollow_octagon_extrude.py | 6 +- .../microstructure/tiles/inverse_cross_3d.py | 4 +- splinepy/microstructure/tiles/snappy.py | 2 + tests/test_microstructure.py | 76 +++++++++++++++---- 14 files changed, 104 insertions(+), 21 deletions(-) diff --git a/splinepy/microstructure/tiles/armadillo.py b/splinepy/microstructure/tiles/armadillo.py index acf8828be..7137f6565 100644 --- a/splinepy/microstructure/tiles/armadillo.py +++ b/splinepy/microstructure/tiles/armadillo.py @@ -30,6 +30,7 @@ class Armadillo(_TileBase): "z_max", ] _parameter_bounds = [[0.0, 0.5]] + _parameters_shape = (1, 1) def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/chi.py b/splinepy/microstructure/tiles/chi.py index 707484c38..dcc2c971f 100644 --- a/splinepy/microstructure/tiles/chi.py +++ b/splinepy/microstructure/tiles/chi.py @@ -19,8 +19,8 @@ class Chi(_TileBase): _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 1 _sensitivities_implemented = True - _parameter_bounds = [[-_np.pi / 2, _np.pi / 2]] + _parameters_shape = (1, 1) def create_tile( self, @@ -51,7 +51,7 @@ def create_tile( ) parameters = _np.array([[_np.pi / 8]]) else: - angle_bounds = self._parameter_bounds["parameters"] + angle_bounds = self._parameter_bounds[0] if not ( _np.all(parameters >= angle_bounds[0]) and _np.all(parameters < angle_bounds[1]) diff --git a/splinepy/microstructure/tiles/cross_2d.py b/splinepy/microstructure/tiles/cross_2d.py index 147da3cee..2fe51c2be 100644 --- a/splinepy/microstructure/tiles/cross_2d.py +++ b/splinepy/microstructure/tiles/cross_2d.py @@ -31,6 +31,7 @@ class Cross2D(_TileBase): _parameter_bounds = [ [0.0, 0.5] ] * 4 # valid for default center_expansion=1.0 + _parameters_shape = (4, 1) def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index 435ac3811..fba595111 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -33,6 +33,7 @@ class Cross3D(_TileBase): _parameter_bounds = [ [0.0, 0.5] ] * 6 # valid for default center_expansion=1.0 + _parameters_shape = (6, 1) def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index 535592aa4..22c0f7959 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -33,6 +33,7 @@ class Cross3DLinear(_TileBase): _parameter_bounds = [ [0.0, 0.5] ] * 6 # valid for default center_expansion=1.0 + _parameters_shape = (6, 1) def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/cube_void.py b/splinepy/microstructure/tiles/cube_void.py index a8da85df6..6fd8e04c6 100644 --- a/splinepy/microstructure/tiles/cube_void.py +++ b/splinepy/microstructure/tiles/cube_void.py @@ -26,6 +26,15 @@ class CubeVoid(_TileBase): _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 4 _sensitivities_implemented = True + # TODO: clever parameter bounds and checks if given parametrization would + # still lie in unit cube + _parameter_bounds = [ + [0.0, 1.0], + [0.0, 1.0], + [-_np.pi / 2, _np.pi / 2], + [-_np.pi / 2, _np.pi / 2], + ] + _parameters_shape = (1, 1, 4) # Aux values _sphere_ctps = _np.array( @@ -92,7 +101,9 @@ def create_tile( derivatives : list> / None """ # set to default if nothing is given if parameters is None: - parameters = _np.array([0.5, 0.5, 0, 0]).reshape(1, 1, 4) + parameters = _np.array([0.5, 0.5, 0, 0]).reshape( + self._parameters_shape + ) # Create center ellipsoid # RotY * RotX * DIAG(r_x, r_yz) * T_base diff --git a/splinepy/microstructure/tiles/double_lattice.py b/splinepy/microstructure/tiles/double_lattice.py index ffb2166b2..e040078f9 100644 --- a/splinepy/microstructure/tiles/double_lattice.py +++ b/splinepy/microstructure/tiles/double_lattice.py @@ -21,6 +21,8 @@ class DoubleLattice(_TileBase): _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 2 _sensitivities_implemented = True + _parameter_bounds = [[0.0, 1 / (2 * (1 + _np.sqrt(2)))]] * 2 + _parameters_shape = (1, 2) def create_tile( self, diff --git a/splinepy/microstructure/tiles/ellips_v_oid.py b/splinepy/microstructure/tiles/ellips_v_oid.py index ec7a5cfe9..4f5c830c6 100644 --- a/splinepy/microstructure/tiles/ellips_v_oid.py +++ b/splinepy/microstructure/tiles/ellips_v_oid.py @@ -28,6 +28,16 @@ class EllipsVoid(_TileBase): _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 4 _sensitivities_implemented = True + # TODO: clever parameter bounds and checks if given parametrization would + # still lie in unit cube + # Due to ellipsoid, control points very easily lie outside unit cube + _parameter_bounds = [ + [0.0, 1.0], + [0.0, 1.0], + [-_np.pi / 2, _np.pi / 2], + [-_np.pi / 2, _np.pi / 2], + ] + _parameters_shape = (1, 1, 4) # Aux values _c0 = 0.5 / 3**0.5 diff --git a/splinepy/microstructure/tiles/hollow_cube.py b/splinepy/microstructure/tiles/hollow_cube.py index 7b37daa68..36353d417 100644 --- a/splinepy/microstructure/tiles/hollow_cube.py +++ b/splinepy/microstructure/tiles/hollow_cube.py @@ -29,6 +29,8 @@ class HollowCube(_TileBase): ) _n_info_per_eval_point = 1 _sensitivities_implemented = True + _parameter_bounds = [[0.0, 0.5]] * 7 + _parameters_shape = (7, 1) def create_tile( self, diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index 44b31a65a..1b9489fcc 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -20,6 +20,8 @@ class HollowOctagon(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = False _closure_directions = ["x_min", "x_max", "y_min", "y_max"] + _parameter_bounds = [[0.0, 0.5]] + _parameters_shape = (1, 1) def _closing_tile( self, diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index dbf5f1901..1c935be0f 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -20,6 +20,8 @@ class HollowOctagonExtrude(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = False _closure_directions = ["x_min", "x_max", "y_min", "y_max"] + _parameter_bounds = [[0.0, 0.5]] + _parameters_shape = (1, 1) def create_tile( self, @@ -75,9 +77,9 @@ def create_tile( self.check_params(parameters) v_h_void = parameters[0, 0] - if not ((v_h_void > 0.01) and (v_h_void < 0.5)): + if not ((v_h_void > 0.0) and (v_h_void < 0.5)): raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" + "The thickness of the wall must be in (0.0 and 0.5)" ) v_zero = 0.0 diff --git a/splinepy/microstructure/tiles/inverse_cross_3d.py b/splinepy/microstructure/tiles/inverse_cross_3d.py index a633632fc..6e4f0e010 100644 --- a/splinepy/microstructure/tiles/inverse_cross_3d.py +++ b/splinepy/microstructure/tiles/inverse_cross_3d.py @@ -34,6 +34,8 @@ class InverseCross3D(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = False _closure_directions = ["z_min", "z_max"] + _parameter_bounds = [[0.2, 0.3]] * 6 # For default values + _parameters_shape = (6, 1) def _closing_tile( self, @@ -909,7 +911,7 @@ def create_tile( branches separator_distance : float Control point distance to separation layer of biquadratic degrees, - determines the minimum branch thickness (defaults to 0.4) + determines the minimum branch thickness (defaults to 0.3) center_expansion : float thickness of center is expanded by a factor (default to 1.0), which determines the maximum branch thickness diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index 60cab586f..84eba670d 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -22,6 +22,8 @@ class Snappy(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = False _closure_directions = ["y_min", "y_max"] + _parameter_bounds = [] + _parameters_shape = () def _closing_tile( self, diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index b287b5b09..8d7846ded 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -3,12 +3,43 @@ import numpy as np import splinepy.microstructure as ms +from splinepy.utils.data import cartesian_product as _cartesian_product EPS = 1e-8 all_tile_classes = list(ms.tiles.everything().values()) +def check_control_points(tile_patches): + """Check if all of tile's control points all lie within unit square/cube""" + # Go through all patches + for tile_patch in tile_patches: + cps = tile_patch.control_points + assert np.all( + (cps >= 0.0 - EPS) & (cps <= 1.0 + EPS) + ), "Control points of tile must lie inside the unit square/cube" + + +def make_bounds_feasible(bounds): + """Bounds are given are understood as open set of min. and max. values. Therefore, + convert bounds to open set of these values. + + Parameters + ------------ + bounds: list> + Values of bounds + + Returns + ---------- + feasible_bounds: np.ndarray + Values of bounds + """ + feasible_bounds = [ + [min_value + EPS, max_value - EPS] for min_value, max_value in bounds + ] + return np.array(feasible_bounds) + + def test_tile_class(): """Checks if all tile classes have the appropriate members and functions.""" required_class_variables = { @@ -60,23 +91,41 @@ def test_tile_class(): def test_tile_bounds(): """Test if tile is still in unit cube at the bounds. Currently only checks default parameter values.""" - # TODO: also check non-default parameters - - def check_control_points(tile_patches): - """Check if all of tile's control points all lie within unit square/cube""" - # Go through all patches - for tile_patch in tile_patches: - cps = tile_patch.control_points - assert np.all( - (cps >= 0.0) & (cps <= 1.0) - ), "Control points of tile must lie inside the unit square/cube" + # Skip certain tile classes for testing + skip_tiles = [ + ms.tiles.EllipsVoid, # Control points easily lie outside unitcube + ms.tiles.Snappy, # Has no parameters + ] for tile_class in all_tile_classes: + # Skip certain classes for testing + skip_tile_class = False + for skip_tile in skip_tiles: + if tile_class is skip_tile: + skip_tile_class = True + break + if skip_tile_class: + continue + tile_creator = tile_class() # Create tile with default parameters tile_patches, _ = tile_creator.create_tile() check_control_points(tile_patches) + # Go through all extremes of parameters and ensure that also they create + # tiles within unit square/cube + feasible_parameter_bounds = make_bounds_feasible( + tile_creator._parameter_bounds + ) + all_parameter_bounds = _cartesian_product(feasible_parameter_bounds) + for parameter_extremes in all_parameter_bounds: + tile_patches, _ = tile_creator.create_tile( + parameters=parameter_extremes.reshape( + tile_creator._parameters_shape + ) + ) + check_control_points(tile_patches) + def test_tile_derivatives(): """Testing the correctness of the tile derivatives using Finite Differences""" @@ -105,8 +154,5 @@ def test_tile_closure(): assert ( sensitivities is None ), "Ensure sensitivities for closure are None if no sensitivities are asked" - for tile_patch in tile_patches: - cps = tile_patch.control_points - assert np.all( - (cps >= 0.0) & (cps <= 1.0) - ), "Control points of tile must lie inside the unit square/cube" + + check_control_points(tile_patches) From 12f0ea716930ce9cd05253f76f498a643fdb2f6a Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 12 Aug 2024 16:10:04 +0200 Subject: [PATCH 080/171] Add test for non-default parameters plus closure of microtiles --- .../microstructure/tiles/hollow_octagon.py | 4 +-- tests/test_microstructure.py | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index 1b9489fcc..f3dbfd5e8 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -77,9 +77,9 @@ def _closing_tile( ) v_h_void = parameters[0, 0] - if not ((v_h_void > 0.01) and (v_h_void < 0.5)): + if not ((v_h_void > 0.0) and (v_h_void < 0.5)): raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" + "The thickness of the wall must be in (0.0 and 0.5)" ) spline_list = [] diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 8d7846ded..7e83795d3 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -140,7 +140,12 @@ def test_tile_derivatives(): def test_tile_closure(): """Check if closing tiles also lie in unit cube. Maybe also check derivatives.""" - # TODO: check closure also for extreme values of parameters + # Skip certain tile classes for testing + skip_tiles = [ + ms.tiles.EllipsVoid, # Control points easily lie outside unitcube + ms.tiles.Snappy, # Has no parameters + ] + for tile_class in all_tile_classes: # Skip tile if if does not support closure if "_closure_directions" not in dir(tile_class): @@ -156,3 +161,30 @@ def test_tile_closure(): ), "Ensure sensitivities for closure are None if no sensitivities are asked" check_control_points(tile_patches) + + # Also check non-default parameters + # Skip certain classes for testing + skip_tile_class = False + for skip_tile in skip_tiles: + if tile_class is skip_tile: + skip_tile_class = True + break + if skip_tile_class: + continue + + # Go through all extremes of parameters and ensure that also they create + # tiles within unit square/cube + feasible_parameter_bounds = make_bounds_feasible( + tile_creator._parameter_bounds + ) + all_parameter_bounds = _cartesian_product(feasible_parameter_bounds) + # Go through all implemented closure directions + for closure_direction in tile_creator._closure_directions: + for parameter_extremes in all_parameter_bounds: + tile_patches, _ = tile_creator.create_tile( + parameters=parameter_extremes.reshape( + tile_creator._parameters_shape + ), + closure=closure_direction, + ) + check_control_points(tile_patches) From 6c9cf844b7b570ac8c1ea0ed326e1919e77bb0a6 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 12 Aug 2024 16:19:21 +0200 Subject: [PATCH 081/171] Restructure microstructure tests --- tests/test_microstructure.py | 57 +++++++++++++++++------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 7e83795d3..a37b43b4e 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -9,6 +9,12 @@ all_tile_classes = list(ms.tiles.everything().values()) +# Skip certain tile classes for parameters testing +skip_tiles = [ + ms.tiles.EllipsVoid, # Control points easily lie outside unitcube + ms.tiles.Snappy, # Has no "parameters" +] + def check_control_points(tile_patches): """Check if all of tile's control points all lie within unit square/cube""" @@ -48,6 +54,8 @@ def test_tile_class(): "_evaluation_points": np.ndarray, "_n_info_per_eval_point": int, "_sensitivities_implemented": bool, + "_parameter_bounds": list, + "_parameters_shape": tuple, } # Go through all available tiles @@ -89,15 +97,14 @@ def test_tile_class(): def test_tile_bounds(): - """Test if tile is still in unit cube at the bounds. Currently only checks - default parameter values.""" - # Skip certain tile classes for testing - skip_tiles = [ - ms.tiles.EllipsVoid, # Control points easily lie outside unitcube - ms.tiles.Snappy, # Has no parameters - ] - + """Test if tile is still in unit cube at the bounds. Checks default and also + non-default parameter values.""" for tile_class in all_tile_classes: + tile_creator = tile_class() + # Create tile with default parameters + tile_patches, _ = tile_creator.create_tile() + check_control_points(tile_patches) + # Skip certain classes for testing skip_tile_class = False for skip_tile in skip_tiles: @@ -107,11 +114,6 @@ def test_tile_bounds(): if skip_tile_class: continue - tile_creator = tile_class() - # Create tile with default parameters - tile_patches, _ = tile_creator.create_tile() - check_control_points(tile_patches) - # Go through all extremes of parameters and ensure that also they create # tiles within unit square/cube feasible_parameter_bounds = make_bounds_feasible( @@ -127,24 +129,8 @@ def test_tile_bounds(): check_control_points(tile_patches) -def test_tile_derivatives(): - """Testing the correctness of the tile derivatives using Finite Differences""" - # TODO - for tile_class in all_tile_classes: - tile_creator = tile_class() - # Skip test if tile class has no implemented sensitivities - if not tile_creator._sensitivities_implemented: - continue - pass - - def test_tile_closure(): - """Check if closing tiles also lie in unit cube. Maybe also check derivatives.""" - # Skip certain tile classes for testing - skip_tiles = [ - ms.tiles.EllipsVoid, # Control points easily lie outside unitcube - ms.tiles.Snappy, # Has no parameters - ] + """Check if closing tiles also lie in unit cube. TODO: also check derivatives.""" for tile_class in all_tile_classes: # Skip tile if if does not support closure @@ -188,3 +174,14 @@ def test_tile_closure(): closure=closure_direction, ) check_control_points(tile_patches) + + +def test_tile_derivatives(): + """Testing the correctness of the tile derivatives using Finite Differences""" + # TODO + for tile_class in all_tile_classes: + tile_creator = tile_class() + # Skip test if tile class has no implemented sensitivities + if not tile_creator._sensitivities_implemented: + continue + pass From 7050de6d6a359c88f8f0470e7aec21bb17b28907 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 26 Aug 2024 10:42:31 +0200 Subject: [PATCH 082/171] Add test for tile derivatives (WIP) --- splinepy/microstructure/tiles/chi.py | 2 +- splinepy/microstructure/tiles/cube_void.py | 2 +- splinepy/microstructure/tiles/ellips_v_oid.py | 2 +- tests/test_microstructure.py | 97 ++++++++++++++++++- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/splinepy/microstructure/tiles/chi.py b/splinepy/microstructure/tiles/chi.py index dcc2c971f..827c7b8d3 100644 --- a/splinepy/microstructure/tiles/chi.py +++ b/splinepy/microstructure/tiles/chi.py @@ -16,7 +16,7 @@ class Chi(_TileBase): _dim = 2 _para_dim = 1 - _evaluation_points = _np.array([[0.5, 0.5]]) + _evaluation_points = _np.array([[0.5]]) _n_info_per_eval_point = 1 _sensitivities_implemented = True _parameter_bounds = [[-_np.pi / 2, _np.pi / 2]] diff --git a/splinepy/microstructure/tiles/cube_void.py b/splinepy/microstructure/tiles/cube_void.py index 6fd8e04c6..c435b6a88 100644 --- a/splinepy/microstructure/tiles/cube_void.py +++ b/splinepy/microstructure/tiles/cube_void.py @@ -34,7 +34,7 @@ class CubeVoid(_TileBase): [-_np.pi / 2, _np.pi / 2], [-_np.pi / 2, _np.pi / 2], ] - _parameters_shape = (1, 1, 4) + _parameters_shape = (1, 4) # Aux values _sphere_ctps = _np.array( diff --git a/splinepy/microstructure/tiles/ellips_v_oid.py b/splinepy/microstructure/tiles/ellips_v_oid.py index 4f5c830c6..84025d8bf 100644 --- a/splinepy/microstructure/tiles/ellips_v_oid.py +++ b/splinepy/microstructure/tiles/ellips_v_oid.py @@ -37,7 +37,7 @@ class EllipsVoid(_TileBase): [-_np.pi / 2, _np.pi / 2], [-_np.pi / 2, _np.pi / 2], ] - _parameters_shape = (1, 1, 4) + _parameters_shape = (1, 4) # Aux values _c0 = 0.5 / 3**0.5 diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index a37b43b4e..70bf4b2e5 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -176,12 +176,101 @@ def test_tile_closure(): check_control_points(tile_patches) -def test_tile_derivatives(): - """Testing the correctness of the tile derivatives using Finite Differences""" - # TODO +def test_tile_derivatives(np_rng, heps=1e-8): + """Testing the correctness of the tile derivatives using Finite Differences + + Parameters + --------- + heps: float + Perturbation size for finite difference evaluation + """ + # TODO: right now Chi, EllipsVoid and CubeVoid show wrong derivatives + skip_classes = [ms.tiles.Chi, ms.tiles.EllipsVoid, ms.tiles.CubeVoid] + for tile_class in all_tile_classes: + # TODO: right now skip classes with faultily implemented derivatives + if tile_class in skip_classes: + continue + tile_creator = tile_class() # Skip test if tile class has no implemented sensitivities if not tile_creator._sensitivities_implemented: continue - pass + # Choose parameter(s) as mean of extreme values + parameter_bounds = tile_creator._parameter_bounds + parameters = np.mean(np.array(parameter_bounds), axis=1).reshape( + tile_creator._parameters_shape + ) + # Create 3D identity matrix + n_eval_points = tile_creator._evaluation_points.shape[0] + n_info_per_eval_point = tile_creator._n_info_per_eval_point + parameter_sensitivities = np.zeros( + (n_eval_points, n_info_per_eval_point, 1) + ) + idx = np.arange(np.min(parameter_sensitivities.shape)) + parameter_sensitivities[idx, idx, :] = 1 + splines, derivatives = tile_creator.create_tile( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) + # Evaluate splines and derivatives at random points + eval_points = np_rng.random((4, splines[0].para_dim)) + deriv_evaluations = [ + deriv.evaluate(eval_points) for deriv in derivatives[0] + ] + spline_orig_evaluations = [ + spl.evaluate(eval_points) for spl in splines + ] + + parameters_perturbed = parameters.copy() + parameters_perturbed += heps + splines_perturbed, _ = tile_creator.create_tile( + parameters=parameters_perturbed + ) + spline_perturbed_evaluations = [ + spl.evaluate(eval_points) for spl in splines_perturbed + ] + fd_sensitivities = [ + (spl_pert - spl_orig) / heps + for spl_pert, spl_orig in zip( + spline_perturbed_evaluations, spline_orig_evaluations + ) + ] + + # Go through all the parameters individually + for i_parameter in range(n_info_per_eval_point): + # Get derivatives w.r.t. one parameter + parameter_sensitivities = np.zeros( + (n_eval_points, n_info_per_eval_point, 1) + ) + parameter_sensitivities[:, i_parameter, :] = 1 + _, derivatives = tile_creator.create_tile( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) + deriv_evaluations = [ + deriv.evaluate(eval_points) for deriv in derivatives[0] + ] + # Perform finite difference evaluation + parameters_perturbed = parameters.copy() + parameters_perturbed[:, i_parameter] += heps + splines_perturbed, _ = tile_creator.create_tile( + parameters=parameters_perturbed + ) + spline_perturbed_evaluations = [ + spl.evaluate(eval_points) for spl in splines_perturbed + ] + fd_sensitivities = [ + (spl_pert - spl_orig) / heps + for spl_pert, spl_orig in zip( + spline_perturbed_evaluations, spline_orig_evaluations + ) + ] + # Check every patch + for deriv_orig, deriv_fd in zip( + deriv_evaluations, fd_sensitivities + ): + assert np.allclose(deriv_orig, deriv_fd), ( + "Implemented derivative calculation does not match the derivative " + + "obtained using Finite Differences" + ) From 6d01d7bd6fcdabb7ae57cee1e412c384bcb3e26c Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 26 Aug 2024 14:28:44 +0200 Subject: [PATCH 083/171] Add derivative of HollowOctagon tile --- .../microstructure/tiles/hollow_octagon.py | 202 ++++++++++-------- tests/test_microstructure.py | 15 +- 2 files changed, 122 insertions(+), 95 deletions(-) diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index f3dbfd5e8..b05f6b74e 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -18,7 +18,7 @@ class HollowOctagon(_TileBase): _para_dim = 2 _evaluation_points = _np.array([[0.5, 0.5]]) _n_info_per_eval_point = 1 - _sensitivities_implemented = False + _sensitivities_implemented = True _closure_directions = ["x_min", "x_max", "y_min", "y_max"] _parameter_bounds = [[0.0, 0.5]] _parameters_shape = (1, 1) @@ -87,7 +87,7 @@ def _closing_tile( v_one_half = 0.5 v_one = 1.0 v_outer_c_h = contact_length * 0.5 - v_inner_c_h = contact_length * parameters[0, 0] + v_inner_c_h = contact_length * v_h_void if closure == "x_min": # set points: @@ -407,7 +407,7 @@ def _closing_tile( def create_tile( self, parameters=None, - parameter_sensitivities=None, # TODO + parameter_sensitivities=None, contact_length=0.2, closure=None, **kwargs, # noqa ARG002 @@ -456,13 +456,15 @@ def create_tile( ) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) - v_h_void = parameters[0, 0] if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): raise ValueError( "The thickness of the wall must be in (0.01 and 0.49)" @@ -476,103 +478,125 @@ def create_tile( closure=closure, ) - v_zero = 0.0 - v_one_half = 0.5 - v_one = 1.0 - v_outer_c_h = contact_length * 0.5 - v_inner_c_h = contact_length * parameters[0, 0] + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + v_h_void = parameters[0, 0] + v_outer_c_h = contact_length * 0.5 + v_inner_c_h = contact_length * v_h_void + else: + v_zero = 0.0 + v_one_half = 0.0 + v_one = 0.0 + v_h_void = parameter_sensitivities[0, 0, i_derivative - 1] + v_outer_c_h = 0.0 + v_inner_c_h = contact_length * v_h_void + + spline_list = [] - spline_list = [] + # set points: + right = _np.array( + [ + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + ] + ) - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) + top = _np.array( + [ + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) + bottom_left = _np.array( + [ + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, -v_outer_c_h + v_one_half], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + ] + ) - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_zero], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + spline_list.append(_Bezier(degrees=[1, 1], control_points=right)) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right)) + spline_list.append( + _Bezier(degrees=[1, 1], control_points=right_top) + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right_top)) + spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom)) - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom)) + spline_list.append( + _Bezier(degrees=[1, 1], control_points=bottom_left) + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom_left)) + spline_list.append(_Bezier(degrees=[1, 1], control_points=left)) - spline_list.append(_Bezier(degrees=[1, 1], control_points=left)) + spline_list.append( + _Bezier(degrees=[1, 1], control_points=top_left) + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=top_left)) + spline_list.append(_Bezier(degrees=[1, 1], control_points=top)) - spline_list.append(_Bezier(degrees=[1, 1], control_points=top)) + spline_list.append( + _Bezier(degrees=[1, 1], control_points=bottom_right) + ) - spline_list.append( - _Bezier(degrees=[1, 1], control_points=bottom_right) - ) + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) - return (spline_list, None) + return (splines, derivatives) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 70bf4b2e5..1a2b54371 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -196,11 +196,14 @@ def test_tile_derivatives(np_rng, heps=1e-8): # Skip test if tile class has no implemented sensitivities if not tile_creator._sensitivities_implemented: continue - # Choose parameter(s) as mean of extreme values - parameter_bounds = tile_creator._parameter_bounds - parameters = np.mean(np.array(parameter_bounds), axis=1).reshape( - tile_creator._parameters_shape - ) + + # Choose random parameter(s) within bounds + parameter_bounds = np.array(tile_creator._parameter_bounds) + parameters = parameter_bounds[:, 0] + np_rng.random( + len(parameter_bounds) + ) * np.ptp(parameter_bounds, axis=1) + parameters = parameters.reshape(tile_creator._parameters_shape) + # Create 3D identity matrix n_eval_points = tile_creator._evaluation_points.shape[0] n_info_per_eval_point = tile_creator._n_info_per_eval_point @@ -213,7 +216,7 @@ def test_tile_derivatives(np_rng, heps=1e-8): parameters=parameters, parameter_sensitivities=parameter_sensitivities, ) - # Evaluate splines and derivatives at random points + # Evaluate splines and derivatives at multiple random points eval_points = np_rng.random((4, splines[0].para_dim)) deriv_evaluations = [ deriv.evaluate(eval_points) for deriv in derivatives[0] From ce88fdcfa871449a2251d94a97b8a4ba4527a469 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 26 Aug 2024 14:40:26 +0200 Subject: [PATCH 084/171] Add derivatives for closure of HollowOctagon tile --- .../microstructure/tiles/hollow_octagon.py | 631 +++++++++--------- 1 file changed, 318 insertions(+), 313 deletions(-) diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index b05f6b74e..22ace6555 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -26,7 +26,7 @@ class HollowOctagon(_TileBase): def _closing_tile( self, parameters=None, - parameter_sensitivities=None, # TODO + parameter_sensitivities=None, contact_length=0.2, closure=None, ): @@ -70,11 +70,14 @@ def _closing_tile( ) self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None v_h_void = parameters[0, 0] if not ((v_h_void > 0.0) and (v_h_void < 0.5)): @@ -82,327 +85,339 @@ def _closing_tile( "The thickness of the wall must be in (0.0 and 0.5)" ) - spline_list = [] - v_zero = 0.0 - v_one_half = 0.5 - v_one = 1.0 - v_outer_c_h = contact_length * 0.5 - v_inner_c_h = contact_length * v_h_void - - if closure == "x_min": - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) - - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) - - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) - - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) - - left = _np.array( - [ - [v_zero, v_zero], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_one], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + v_outer_c_h = contact_length * 0.5 + v_inner_c_h = contact_length * v_h_void + else: + v_zero = 0.0 + v_one_half = 0.0 + v_one = 0.0 + v_h_void = parameter_sensitivities[0, 0, i_derivative - 1] + v_outer_c_h = 0.0 + v_inner_c_h = contact_length * v_h_void - top_left = _np.array( - [ - [v_zero, v_one], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + spline_list = [] - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + if closure == "x_min": + # set points: + right = _np.array( + [ + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + ] + ) - elif closure == "x_max": - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, v_zero], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_one], - ] - ) + top = _np.array( + [ + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_one], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) + bottom_left = _np.array( + [ + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, v_zero], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) + left = _np.array( + [ + [v_zero, v_zero], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, v_one], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) + top_left = _np.array( + [ + [v_zero, v_one], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_zero], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + ] + ) - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + elif closure == "x_max": + right = _np.array( + [ + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, v_zero], + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_one], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, v_zero], - ] - ) + right_top = _np.array( + [ + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_one], + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + ] + ) - elif closure == "y_min": - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) + top = _np.array( + [ + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) + bottom_left = _np.array( + [ + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, -v_outer_c_h + v_one_half], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_zero, v_zero], - ] - ) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_zero], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, v_zero], + ] + ) - bottom = _np.array( - [ - [v_one, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_zero, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + elif closure == "y_min": + # set points: + right = _np.array( + [ + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_one, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + ] + ) - elif closure == "y_max": - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) + top = _np.array( + [ + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_one, v_one], - ] - ) + bottom_left = _np.array( + [ + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, -v_outer_c_h + v_one_half], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_zero, v_zero], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_one, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_zero, v_one], - ] - ) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + [-v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_one, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_zero, v_zero], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_zero, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_one, v_zero], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + ] + ) - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + elif closure == "y_max": + # set points: + right = _np.array( + [ + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_one, v_outer_c_h + v_one_half], + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_one, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right)) + top = _np.array( + [ + [v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_one, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + [v_zero, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right_top)) + bottom_left = _np.array( + [ + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, -v_outer_c_h + v_one_half], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom)) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom_left)) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half], + [v_zero, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=left)) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [-v_outer_c_h + v_one_half, v_zero], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=top_left)) + bottom_right = _np.array( + [ + [v_inner_c_h + v_one_half, -v_h_void + v_one_half], + [v_outer_c_h + v_one_half, v_zero], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half], + [v_one, -v_outer_c_h + v_one_half], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=top)) + for control_points in [ + right, + right_top, + bottom, + bottom_left, + left, + top_left, + top, + bottom_right, + ]: + spline_list.append( + _Bezier(degrees=[1, 1], control_points=control_points) + ) - spline_list.append( - _Bezier(degrees=[1, 1], control_points=bottom_right) - ) + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) - return (spline_list, None) + return (splines, derivatives) def create_tile( self, @@ -473,7 +488,7 @@ def create_tile( if closure is not None: return self._closing_tile( parameters=parameters, - parameter_sensitivities=parameter_sensitivities, # TODO + parameter_sensitivities=parameter_sensitivities, contact_length=contact_length, closure=closure, ) @@ -570,29 +585,19 @@ def create_tile( ] ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right)) - - spline_list.append( - _Bezier(degrees=[1, 1], control_points=right_top) - ) - - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom)) - - spline_list.append( - _Bezier(degrees=[1, 1], control_points=bottom_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1], control_points=left)) - - spline_list.append( - _Bezier(degrees=[1, 1], control_points=top_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1], control_points=top)) - - spline_list.append( - _Bezier(degrees=[1, 1], control_points=bottom_right) - ) + for control_points in [ + right, + right_top, + bottom, + bottom_left, + left, + top_left, + top, + bottom_right, + ]: + spline_list.append( + _Bezier(degrees=[1, 1], control_points=control_points) + ) if i_derivative == 0: splines = spline_list.copy() From 655c37d0b9195ff8b79a806076a0006230be9157 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 26 Aug 2024 15:22:35 +0200 Subject: [PATCH 085/171] Add derivatives for Armadillo tile --- splinepy/microstructure/tiles/armadillo.py | 10086 +++++++++---------- 1 file changed, 5028 insertions(+), 5058 deletions(-) diff --git a/splinepy/microstructure/tiles/armadillo.py b/splinepy/microstructure/tiles/armadillo.py index 7137f6565..e9fe2a50c 100644 --- a/splinepy/microstructure/tiles/armadillo.py +++ b/splinepy/microstructure/tiles/armadillo.py @@ -20,7 +20,7 @@ class Armadillo(_TileBase): _dim = 3 _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 1 - _sensitivities_implemented = False + _sensitivities_implemented = True _closure_directions = [ "x_min", "x_max", @@ -35,7 +35,7 @@ class Armadillo(_TileBase): def _closing_tile( self, parameters=None, - parameter_sensitivities=None, # TODO + parameter_sensitivities=None, contact_length=0.3, closure=None, **kwargs, # noqa ARG002 @@ -85,4088 +85,5043 @@ def _closing_tile( ) self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): raise ValueError( "The thickness of the wall must be in (0.01 and 0.49)" ) - v_wall_thickness = parameters[0, 0] - spline_list = [] - v_zero = 0.0 - v_one_half = 0.5 - v_one = 1.0 - v_half_contact_length = contact_length * 0.5 - v_inner_half_contact_length = contact_length * parameters[0, 0] - - if closure == "x_min": - # set points: - right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + v_wall_thickness = parameters[0, 0] + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + v_half_contact_length = contact_length * 0.5 + v_inner_half_contact_length = contact_length * v_wall_thickness + else: + v_wall_thickness = parameter_sensitivities[ + 0, 0, i_derivative - 1 ] - ) + v_zero = 0.0 + v_one_half = 0.0 + v_one = 0.0 + v_half_contact_length = 0.0 + v_inner_half_contact_length = contact_length * v_wall_thickness + + spline_list = [] + + if closure == "x_min": + # set points: + right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) - connection_front_right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) + connection_front_right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) - front = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) + front = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) - connection_back_left = _np.array( - [ - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_zero, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - ] - ) + connection_back_left = _np.array( + [ + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + ] + ) - left = _np.array( - [ - [ - v_zero, - v_one, - v_one, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_one, - v_zero, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_zero, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + left = _np.array( + [ + [ + v_zero, + v_one, + v_one, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_front_left = _np.array( - [ - [ - v_zero, - v_one, - v_one, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_one, - v_zero, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_front_left = _np.array( + [ + [ + v_zero, + v_one, + v_one, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - back = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + back = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_back_right = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) + connection_back_right = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) - bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - top = _np.array( - [ - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - ] - ) + top = _np.array( + [ + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + ] + ) - connection_front_bottom = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_front_bottom = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - connection_front_top = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - ] - ) + connection_front_top = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + ] + ) - connection_back_bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_back_bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_back_top = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + connection_back_top = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + ] + ) - connection_top_right = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + connection_top_right = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) - connection_top_left = _np.array( - [ - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_zero, - v_one, - v_one, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + connection_top_left = _np.array( + [ + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_zero, + v_one, + v_one, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) - connection_bottom_left = _np.array( - [ - [ - v_zero, - v_one, - v_zero, - ], - [ - v_zero, - v_zero, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_bottom_left = _np.array( + [ + [ + v_zero, + v_one, + v_zero, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - connection_bottom_right = _np.array( - [ - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - elif closure == "x_max": - # set points: - right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_zero, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_zero, - ], - ] - ) - - connection_front_right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) - - front = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) - - connection_back_left = _np.array( - [ - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - ] - ) - - left = _np.array( - [ - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_front_left = _np.array( - [ - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - back = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_back_right = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_zero, - ], - ] - ) - - bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - top = _np.array( - [ - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - ] - ) - - connection_front_bottom = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - connection_front_top = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - ] - ) - - connection_back_bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_back_top = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_top_right = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_top_left = _np.array( - [ - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_bottom_left = _np.array( - [ - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - connection_bottom_right = _np.array( - [ - [ - v_one, - v_one, - v_zero, - ], - [ - v_one, - v_zero, - v_zero, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - elif closure == "y_min": - # set points: - right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) - - connection_front_right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) - - front = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) - - connection_back_left = _np.array( - [ - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_zero, - ], - ] - ) - - left = _np.array( - [ - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_front_left = _np.array( - [ - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - back = _np.array( - [ - [ - v_one, - v_zero, - v_one, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_zero, - v_zero, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_back_right = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_zero, - v_zero, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) - - bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - top = _np.array( - [ - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - ] - ) - - connection_front_bottom = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - connection_front_top = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - ] - ) - - connection_back_bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one, - v_zero, - v_zero, - ], - [ - v_zero, - v_zero, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_back_top = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_top_right = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_top_left = _np.array( - [ - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_bottom_left = _np.array( - [ - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - connection_bottom_right = _np.array( - [ - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - elif closure == "y_max": - # set points: - # set points: - right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) - - connection_front_right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_zero, - ], - ] - ) - - front = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_zero, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_one, - v_one, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_one, - v_zero, - ], - ] - ) - - connection_back_left = _np.array( - [ - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - ] - ) - - left = _np.array( - [ - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_bottom_right = _np.array( + [ + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - connection_top_left = _np.array( - [ - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_one, - v_one, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_one, - v_zero, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + elif closure == "x_max": + # set points: + right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_zero, + ], + ] + ) - back = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_front_right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) - connection_back_right = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) + front = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) - bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_back_left = _np.array( + [ + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + ] + ) - top = _np.array( - [ - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - ] - ) + left = _np.array( + [ + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_front_bottom = _np.array( - [ - [ - v_one, - v_one, - v_zero, - ], - [ - v_zero, - v_one, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_front_left = _np.array( + [ + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_front_top = _np.array( - [ - [ - v_one, - v_one, - v_one, - ], - [ - v_zero, - v_one, - v_one, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - ] - ) + back = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_back_right = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_zero, + ], + ] + ) + + bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + top = _np.array( + [ + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + ] + ) + + connection_front_bottom = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_front_top = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + ] + ) + + connection_back_bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_back_top = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_right = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_left = _np.array( + [ + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_bottom_left = _np.array( + [ + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_bottom_right = _np.array( + [ + [ + v_one, + v_one, + v_zero, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + elif closure == "y_min": + # set points: + right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_front_right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) + + front = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_back_left = _np.array( + [ + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_zero, + ], + ] + ) + + left = _np.array( + [ + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_back_bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_front_left = _np.array( + [ + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_back_top = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + back = _np.array( + [ + [ + v_one, + v_zero, + v_one, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_top_right = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + connection_back_right = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) - connection_front_left = _np.array( - [ - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - connection_bottom_left = _np.array( - [ - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + top = _np.array( + [ + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + ] + ) + + connection_front_bottom = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_front_top = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + ] + ) + + connection_back_bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_back_top = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_right = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_left = _np.array( + [ + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_bottom_left = _np.array( + [ + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_bottom_right = _np.array( + [ + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + elif closure == "y_max": + # set points: + # set points: + right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_front_right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_zero, + ], + ] + ) + + front = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_zero, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_one, + v_one, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_one, + v_zero, + ], + ] + ) + + connection_back_left = _np.array( + [ + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + ] + ) + + left = _np.array( + [ + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_top_left = _np.array( + [ + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_one, + v_one, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + back = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_bottom_right = _np.array( - [ - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_back_right = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) - elif closure == "z_max": - # set points: - right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) + bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - connection_front_right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) + top = _np.array( + [ + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + ] + ) - front = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) + connection_front_bottom = _np.array( + [ + [ + v_one, + v_one, + v_zero, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - connection_back_left = _np.array( - [ - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - ] - ) + connection_front_top = _np.array( + [ + [ + v_one, + v_one, + v_one, + ], + [ + v_zero, + v_one, + v_one, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + ] + ) + + connection_back_bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_back_top = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_right = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_front_left = _np.array( + [ + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_bottom_left = _np.array( + [ + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_bottom_right = _np.array( + [ + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + elif closure == "z_max": + # set points: + right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_front_right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) + + front = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_back_left = _np.array( + [ + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + ] + ) + + left = _np.array( + [ + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_front_left = _np.array( + [ + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + back = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_back_right = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) + + bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - left = _np.array( - [ - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + top = _np.array( + [ + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_zero, + v_one, + v_one, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_zero, + v_zero, + v_one, + ], + ] + ) - connection_front_left = _np.array( - [ - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_front_bottom = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) - back = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_front_top = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_one, + ], + [ + v_zero, + v_one, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + ] + ) - connection_back_right = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) + connection_back_bottom = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_back_top = _np.array( + [ + [ + v_one, + v_zero, + v_one, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_right = _np.array( + [ + [ + v_one, + v_one, + v_one, + ], + [ + v_one, + v_zero, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_top_left = _np.array( + [ + [ + v_zero, + v_one, + v_one, + ], + [ + v_zero, + v_zero, + v_one, + ], + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) + + connection_bottom_left = _np.array( + [ + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_bottom_right = _np.array( + [ + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + elif closure == "z_min": + # set points: + # set points: + right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_front_right = _np.array( + [ + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) + + front = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + ] + ) + + connection_back_left = _np.array( + [ + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + ] + ) + + left = _np.array( + [ + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_front_left = _np.array( + [ + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_zero, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + -v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + back = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + ] + ) + + connection_back_right = _np.array( + [ + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + -v_wall_thickness + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half + v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + ] + ) + + bottom = _np.array( + [ + [ + v_one, + v_one, + v_zero, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + top = _np.array( + [ + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + ] + ) + + connection_front_bottom = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half - v_half_contact_length, + ], + [ + v_one, + v_one, + v_zero, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_front_top = _np.array( + [ + [ + v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_one, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + ] + ) - top = _np.array( - [ - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_zero, - v_one, - v_one, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_zero, - v_zero, - v_one, - ], - ] - ) + connection_back_bottom = _np.array( + [ + [ + v_one, + v_zero, + v_zero, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + v_one_half + v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half - v_half_contact_length, + v_zero, + v_one_half - v_half_contact_length, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + ], + ] + ) - connection_front_bottom = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) + connection_back_top = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + -v_half_contact_length + v_one_half, + v_zero, + v_one_half + v_half_contact_length, + ], + [ + v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + [ + -v_inner_half_contact_length + v_one_half, + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + ], + ] + ) - connection_front_top = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_one, - v_one, - ], - [ - v_zero, - v_one, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - ] - ) + connection_top_right = _np.array( + [ + [ + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half + v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) - connection_back_bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - ] - ) + connection_top_left = _np.array( + [ + [ + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + v_one, + ], + [ + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + v_one, + ], + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half + v_wall_thickness, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + ], + ] + ) - connection_back_top = _np.array( - [ - [ - v_one, - v_zero, - v_one, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - ] - ) + connection_bottom_left = _np.array( + [ + [ + v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, + ], + [ + v_zero, + v_one, + v_zero, + ], + [ + v_zero, + v_zero, + v_zero, + ], + [ + v_one_half - v_wall_thickness, + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_wall_thickness, + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half - v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + connection_bottom_right = _np.array( + [ + [ + v_one, + v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_one, + -v_half_contact_length + v_one_half, + v_one_half - v_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_wall_thickness + v_one_half, + -v_inner_half_contact_length + v_one_half, + v_one_half - v_inner_half_contact_length, + ], + [ + v_one, + v_one, + v_zero, + ], + [ + v_one, + v_zero, + v_zero, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half + v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + [ + v_one_half + v_inner_half_contact_length, + v_one_half - v_inner_half_contact_length, + v_one_half - v_wall_thickness, + ], + ] + ) + + for control_points in [ + right, + connection_front_right, + front, + connection_back_left, + left, + connection_front_left, + back, + connection_back_right, + bottom, + top, + connection_front_bottom, + connection_front_top, + connection_back_bottom, + connection_back_top, + connection_top_right, + connection_top_left, + connection_bottom_left, + connection_bottom_right, + ]: + spline_list.append( + _Bezier(degrees=[1, 1, 1], control_points=control_points) + ) + + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) + + return (splines, derivatives) + + def create_tile( + self, + parameters=None, + parameter_sensitivities=None, # TODO + contact_length=0.3, + closure=None, + **kwargs, # noqa ARG002 + ): + """Create a microtile based on the parameters that describe the wall + thicknesses. + + Thickness parameters are used to describe the inner radius of the + outward facing branches + + Parameters + ---------- + parameters : np.array + One evaluation point with one parameter is used. This parameter + describes the thickness of the wall. The parameters must be a + two-dimensional np.array, where the value must be between 0.01 + and 0.49 + parameter_sensitivities: np.ndarray + Describes the parameter sensitivities with respect to some design + variable. In case the design variables directly apply to the + parameter itself, they evaluate as delta_ij + contact_length : float + the length of the wall that contacts the other microstructure + closure : str + parametric dimension that needs to be closed, given in the form + "x_min", "x_max", etc. - connection_top_right = _np.array( - [ - [ - v_one, - v_one, - v_one, - ], - [ - v_one, - v_zero, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] + Returns + ------- + microtile_list : list(splines) + derivative_list : list / None + """ + + if not isinstance(contact_length, float): + raise ValueError("Invalid Type for radius") + + if not ((contact_length > 0) and (contact_length < 0.99)): + raise ValueError("The length of a side must be in (0.01, 0.99)") + + if parameters is None: + self._logd("Setting parameters to default values (0.2)") + parameters = _np.array( + _np.ones( + (len(self._evaluation_points), self._n_info_per_eval_point) + ) + * 0.2 ) - connection_top_left = _np.array( - [ - [ - v_zero, - v_one, - v_one, - ], - [ - v_zero, - v_zero, - v_one, - ], - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] + self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) + + if parameter_sensitivities is not None: + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None + + if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): + raise ValueError( + "The thickness of the wall must be in (0.01 and 0.49)" ) - connection_bottom_left = _np.array( - [ - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] + if closure is not None: + return self._closing_tile( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + contact_length=contact_length, + closure=closure, + **kwargs, ) - connection_bottom_right = _np.array( - [ - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + v_wall_thickness = parameters[0, 0] + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + v_half_contact_length = contact_length * 0.5 + v_inner_half_contact_length = contact_length * v_wall_thickness + else: + v_wall_thickness = parameter_sensitivities[ + 0, 0, i_derivative - 1 ] - ) + v_zero = 0.0 + v_one_half = 0.0 + v_one = 0.0 + v_half_contact_length = 0.0 + v_inner_half_contact_length = contact_length * v_wall_thickness + + spline_list = [] - elif closure == "z_min": - # set points: # set points: right = _np.array( [ @@ -4531,23 +5486,23 @@ def _closing_tile( bottom = _np.array( [ [ - v_one, - v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, v_zero, ], [ - v_zero, - v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, v_zero, ], [ - v_one, - v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, v_zero, ], [ - v_zero, - v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, v_zero, ], [ @@ -4631,13 +5586,13 @@ def _closing_tile( v_one_half - v_half_contact_length, ], [ - v_one, - v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, v_zero, ], [ - v_zero, - v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, v_zero, ], [ @@ -4711,13 +5666,13 @@ def _closing_tile( connection_back_bottom = _np.array( [ [ - v_one, - v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, v_zero, ], [ - v_zero, - v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, v_zero, ], [ @@ -4901,13 +5856,13 @@ def _closing_tile( v_one_half - v_half_contact_length, ], [ - v_zero, - v_one, + v_one_half - v_half_contact_length, + v_one_half + v_half_contact_length, v_zero, ], [ - v_zero, - v_zero, + v_one_half - v_half_contact_length, + v_one_half - v_half_contact_length, v_zero, ], [ @@ -4956,13 +5911,13 @@ def _closing_tile( v_one_half - v_inner_half_contact_length, ], [ - v_one, - v_one, + v_one_half + v_half_contact_length, + v_one_half + v_half_contact_length, v_zero, ], [ - v_one, - v_zero, + v_one_half + v_half_contact_length, + v_one_half - v_half_contact_length, v_zero, ], [ @@ -4978,1018 +5933,33 @@ def _closing_tile( ] ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_bottom_left) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_bottom_right) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_bottom) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_bottom) - ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_top) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_top) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_top_right) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=right)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_right) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=back)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=left)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_top_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=front)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_right) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=bottom)) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=top)) - - return (spline_list, None) - - def create_tile( - self, - parameters=None, - parameter_sensitivities=None, # TODO - contact_length=0.3, - closure=None, - **kwargs, # noqa ARG002 - ): - """Create a microtile based on the parameters that describe the wall - thicknesses. - - Thickness parameters are used to describe the inner radius of the - outward facing branches - - Parameters - ---------- - parameters : np.array - One evaluation point with one parameter is used. This parameter - describes the thickness of the wall. The parameters must be a - two-dimensional np.array, where the value must be between 0.01 - and 0.49 - parameter_sensitivities: np.ndarray - Describes the parameter sensitivities with respect to some design - variable. In case the design variables directly apply to the - parameter itself, they evaluate as delta_ij - contact_length : float - the length of the wall that contacts the other microstructure - closure : str - parametric dimension that needs to be closed, given in the form - "x_min", "x_max", etc. - - Returns - ------- - microtile_list : list(splines) - derivative_list : list / None - """ - - if not isinstance(contact_length, float): - raise ValueError("Invalid Type for radius") - - if not ((contact_length > 0) and (contact_length < 0.99)): - raise ValueError("The length of a side must be in (0.01, 0.99)") - - if parameters is None: - self._logd("Setting parameters to default values (0.2)") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) + for control_points in [ + right, + connection_front_right, + front, + connection_back_left, + left, + connection_front_left, + back, + connection_back_right, + bottom, + top, + connection_front_bottom, + connection_front_top, + connection_back_bottom, + connection_back_top, + connection_top_right, + connection_top_left, + connection_bottom_left, + connection_bottom_right, + ]: + spline_list.append( + _Bezier(degrees=[1, 1, 1], control_points=control_points) ) - * 0.2 - ) - - self.check_params(parameters) - - if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" - ) - - if closure is not None: - return self._closing_tile( - parameters=parameters, - parameter_sensitivities=parameter_sensitivities, - contact_length=contact_length, - closure=closure, - **kwargs, - ) - - v_wall_thickness = parameters[0, 0] - v_zero = 0.0 - v_one_half = 0.5 - v_one = 1.0 - v_half_contact_length = contact_length * 0.5 - v_half_contact_length = contact_length * 0.5 - v_inner_half_contact_length = contact_length * parameters[0, 0] - - spline_list = [] - - # set points: - right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) - - connection_front_right = _np.array( - [ - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) - - front = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - ] - ) - - connection_back_left = _np.array( - [ - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - ] - ) - - left = _np.array( - [ - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_front_left = _np.array( - [ - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_zero, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - -v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - back = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_back_right = _np.array( - [ - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - -v_wall_thickness + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half + v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - ] - ) - - bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - top = _np.array( - [ - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - ] - ) - - connection_front_bottom = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - connection_front_top = _np.array( - [ - [ - v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_one, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - ] - ) - - connection_back_bottom = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_zero, - v_one_half - v_half_contact_length, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - ], - ] - ) - - connection_back_top = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - -v_half_contact_length + v_one_half, - v_zero, - v_one_half + v_half_contact_length, - ], - [ - v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - [ - -v_inner_half_contact_length + v_one_half, - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_top_right = _np.array( - [ - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half + v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_top_left = _np.array( - [ - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_one, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_one, - ], - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half + v_wall_thickness, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - ], - ] - ) - - connection_bottom_left = _np.array( - [ - [ - v_zero, - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_zero, - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - ], - [ - v_one_half - v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half - v_wall_thickness, - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_wall_thickness, - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half - v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - connection_bottom_right = _np.array( - [ - [ - v_one, - v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_one, - -v_half_contact_length + v_one_half, - v_one_half - v_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_wall_thickness + v_one_half, - -v_inner_half_contact_length + v_one_half, - v_one_half - v_inner_half_contact_length, - ], - [ - v_one_half + v_half_contact_length, - v_one_half + v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_half_contact_length, - v_one_half - v_half_contact_length, - v_zero, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half + v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - [ - v_one_half + v_inner_half_contact_length, - v_one_half - v_inner_half_contact_length, - v_one_half - v_wall_thickness, - ], - ] - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=right)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_right) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=back)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=left)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_left) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=front)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_right) - ) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=bottom)) - - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=top)) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_top) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_front_bottom) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_bottom) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_back_top) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_top_right) - ) - - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_top_left) - ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_bottom_left) - ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=connection_bottom_right) - ) + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) - return (spline_list, None) + return (splines, derivatives) From 60ce376e96fc14fd3c6f8331d6cde910192e311c Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 26 Aug 2024 15:38:20 +0200 Subject: [PATCH 086/171] Fix typo which led to error --- examples/show_microstructures.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/show_microstructures.py b/examples/show_microstructures.py index 35d6d22b8..14d74dc04 100644 --- a/examples/show_microstructures.py +++ b/examples/show_microstructures.py @@ -346,13 +346,13 @@ def foo(x): generator.parametrization_function = foo inverse_microstructure = generator.create( - closing_face="z", seperator_distance=0.4, center_expansion=1.3 + closing_face="z", separator_distance=0.4, center_expansion=1.3 ) # Plot the results _, showables_inverse = generator.show( closing_face="z", - seperator_distance=0.4, + separator_distance=0.4, center_expansion=1.3, title="Parametrized Inverse Microstructure", control_points=False, @@ -365,7 +365,7 @@ def foo(x): # Corresponding Structure generator.microtile = splinepy.microstructure.tiles.get("Cross3D") microstructure = generator.create( - closing_face="z", seperator_distance=0.4, center_expansion=1.3 + closing_face="z", separator_distance=0.4, center_expansion=1.3 ) _, showables = generator.show( closing_face="z", From fbacc09af5d6ee021a4105d861c96d1b2e80274b Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 26 Aug 2024 15:56:20 +0200 Subject: [PATCH 087/171] Add derivatives for HollowOctagonExtrude tile w/o closure --- .../tiles/hollow_octagon_extrude.py | 289 ++++++++++-------- 1 file changed, 158 insertions(+), 131 deletions(-) diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index 1c935be0f..16359ef49 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -18,7 +18,8 @@ class HollowOctagonExtrude(_TileBase): _para_dim = 3 _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 1 - _sensitivities_implemented = False + _sensitivities_implemented = True + # TODO: closure in create_tile still missing _closure_directions = ["x_min", "x_max", "y_min", "y_max"] _parameter_bounds = [[0.0, 0.5]] _parameters_shape = (1, 1) @@ -26,7 +27,7 @@ class HollowOctagonExtrude(_TileBase): def create_tile( self, parameters=None, - parameter_sensitivities=None, # TODO + parameter_sensitivities=None, contact_length=0.2, **kwargs, # noqa ARG002 ): @@ -70,11 +71,14 @@ def create_tile( ) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) v_h_void = parameters[0, 0] if not ((v_h_void > 0.0) and (v_h_void < 0.5)): @@ -82,142 +86,165 @@ def create_tile( "The thickness of the wall must be in (0.0 and 0.5)" ) - v_zero = 0.0 - v_one_half = 0.5 - v_one = 1.0 - v_outer_c_h = contact_length * 0.5 - v_inner_c_h = contact_length * parameters[0, 0] - - spline_list = [] - - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], - [v_one, -v_outer_c_h + v_one_half, 0.0], - [v_h_void + v_one_half, v_inner_c_h + v_one_half, 0.0], - [v_one, v_outer_c_h + v_one_half, 0.0], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], - [v_one, -v_outer_c_h + v_one_half, v_one], - [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], - [v_one, v_outer_c_h + v_one_half, v_one], - ] - ) - - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], - [v_one, v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], - [v_outer_c_h + v_one_half, v_one, v_zero], - [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], - [v_one, v_outer_c_h + v_one_half, v_one], - [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], - [v_outer_c_h + v_one_half, v_one, v_one], - ] - ) - - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], - [v_outer_c_h + v_one_half, v_one, v_zero], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], - [-v_outer_c_h + v_one_half, v_one, v_zero], - [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], - [v_outer_c_h + v_one_half, v_one, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], - [-v_outer_c_h + v_one_half, v_one, v_one], - ] - ) + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + v_outer_c_h = contact_length * 0.5 + v_inner_c_h = contact_length * v_h_void + else: + v_zero = 0.0 + v_one_half = 0.0 + v_one = 0.0 + v_h_void = parameter_sensitivities[0, 0, i_derivative - 1] + v_outer_c_h = 0.0 + v_inner_c_h = contact_length * v_h_void + + spline_list = [] - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], - [v_zero, -v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_zero], - [-v_outer_c_h + v_one_half, v_zero, v_zero], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], - [v_zero, -v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], - [-v_outer_c_h + v_one_half, v_zero, v_one], - ] - ) - - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half, v_zero], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], - [v_zero, v_outer_c_h + v_one_half, v_zero], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], - [v_zero, -v_outer_c_h + v_one_half, v_one], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], - [v_zero, v_outer_c_h + v_one_half, v_one], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], - ] - ) - - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half, v_zero], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], - [-v_outer_c_h + v_one_half, v_one, v_zero], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], - [v_zero, v_outer_c_h + v_one_half, v_one], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], - [-v_outer_c_h + v_one_half, v_one, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], - ] - ) - - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_zero], - [-v_outer_c_h + v_one_half, v_zero, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_zero], - [v_outer_c_h + v_one_half, v_zero, v_one], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], - [-v_outer_c_h + v_one_half, v_zero, v_one], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], - ] - ) - - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_zero], - [v_outer_c_h + v_one_half, v_zero, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], - [v_one, -v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], - [v_outer_c_h + v_one_half, v_zero, v_one], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], - [v_one, -v_outer_c_h + v_one_half, v_one], - ] - ) + # set points: + right = _np.array( + [ + [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], + [v_one, -v_outer_c_h + v_one_half, 0.0], + [v_h_void + v_one_half, v_inner_c_h + v_one_half, 0.0], + [v_one, v_outer_c_h + v_one_half, 0.0], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], + [v_one, -v_outer_c_h + v_one_half, v_one], + [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], + [v_one, v_outer_c_h + v_one_half, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=right)) + right_top = _np.array( + [ + [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], + [v_one, v_outer_c_h + v_one_half, v_zero], + [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], + [v_outer_c_h + v_one_half, v_one, v_zero], + [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], + [v_one, v_outer_c_h + v_one_half, v_one], + [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], + [v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=right_top) - ) + top = _np.array( + [ + [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], + [v_outer_c_h + v_one_half, v_one, v_zero], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], + [v_outer_c_h + v_one_half, v_one, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], + [-v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=bottom)) + bottom_left = _np.array( + [ + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], + [-v_outer_c_h + v_one_half, v_zero, v_one], + ] + ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=bottom_left) - ) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_zero], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [-v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], + [v_zero, v_outer_c_h + v_one_half, v_one], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=left)) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half, v_zero], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_zero], + [v_zero, v_outer_c_h + v_one_half, v_one], + [-v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], + [-v_outer_c_h + v_one_half, v_one, v_one], + [-v_inner_c_h + v_one_half, v_h_void + v_one_half, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=top_left)) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_zero], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], + [-v_outer_c_h + v_one_half, v_zero, v_one], + [-v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1, 1], control_points=top)) + bottom_right = _np.array( + [ + [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_zero], + [v_outer_c_h + v_one_half, v_zero, v_zero], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [v_inner_c_h + v_one_half, -v_h_void + v_one_half, v_one], + [v_outer_c_h + v_one_half, v_zero, v_one], + [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], + [v_one, -v_outer_c_h + v_one_half, v_one], + ] + ) + + for control_points in [ + right, + right_top, + top, + bottom_left, + left, + top_left, + bottom, + bottom_right, + ]: + spline_list.append( + _Bezier(degrees=[1, 1, 1], control_points=control_points) + ) - spline_list.append( - _Bezier(degrees=[1, 1, 1], control_points=bottom_right) - ) + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) - return (spline_list, None) + return (splines, derivatives) def _closing_tile( self, From d96d2619d45b93e30d6ce2f5c099a58abeb25af5 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 27 Aug 2024 10:19:32 +0200 Subject: [PATCH 088/171] Add test for tile derivatives with tile closure --- tests/test_microstructure.py | 140 ++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 1a2b54371..73f8e1b72 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -176,16 +176,25 @@ def test_tile_closure(): check_control_points(tile_patches) -def test_tile_derivatives(np_rng, heps=1e-8): - """Testing the correctness of the tile derivatives using Finite Differences +def test_tile_derivatives(np_rng, heps=1e-8, n_test_points=4): + """Testing the correctness of the tile derivatives using Finite Differences. + This includes every closure and no closure, every parameter and every patch + by evaluating at random points and for random parameters. Parameters --------- heps: float Perturbation size for finite difference evaluation + n_test_points: int + Number of testing points in the parametric domain """ - # TODO: right now Chi, EllipsVoid and CubeVoid show wrong derivatives - skip_classes = [ms.tiles.Chi, ms.tiles.EllipsVoid, ms.tiles.CubeVoid] + # TODO: right now Chi, EllipsVoid, CubeVoid and InverseCross show wrong derivatives + skip_classes = [ + ms.tiles.Chi, + ms.tiles.EllipsVoid, + ms.tiles.CubeVoid, + ms.tiles.InverseCross3D, + ] for tile_class in all_tile_classes: # TODO: right now skip classes with faultily implemented derivatives @@ -204,76 +213,69 @@ def test_tile_derivatives(np_rng, heps=1e-8): ) * np.ptp(parameter_bounds, axis=1) parameters = parameters.reshape(tile_creator._parameters_shape) - # Create 3D identity matrix + # Test no closure as well as ... + closure_directions = [None] + # ... every closure implemented + if "_closure_directions" in dir(tile_creator): + closure_directions += tile_creator._closure_directions + + # Retrieve shape values of parameters n_eval_points = tile_creator._evaluation_points.shape[0] n_info_per_eval_point = tile_creator._n_info_per_eval_point - parameter_sensitivities = np.zeros( - (n_eval_points, n_info_per_eval_point, 1) - ) - idx = np.arange(np.min(parameter_sensitivities.shape)) - parameter_sensitivities[idx, idx, :] = 1 - splines, derivatives = tile_creator.create_tile( - parameters=parameters, - parameter_sensitivities=parameter_sensitivities, - ) - # Evaluate splines and derivatives at multiple random points - eval_points = np_rng.random((4, splines[0].para_dim)) - deriv_evaluations = [ - deriv.evaluate(eval_points) for deriv in derivatives[0] - ] - spline_orig_evaluations = [ - spl.evaluate(eval_points) for spl in splines - ] - parameters_perturbed = parameters.copy() - parameters_perturbed += heps - splines_perturbed, _ = tile_creator.create_tile( - parameters=parameters_perturbed - ) - spline_perturbed_evaluations = [ - spl.evaluate(eval_points) for spl in splines_perturbed - ] - fd_sensitivities = [ - (spl_pert - spl_orig) / heps - for spl_pert, spl_orig in zip( - spline_perturbed_evaluations, spline_orig_evaluations + # Test each closure direction + for closure in closure_directions: + # Evaluate tile with given parameter and closure configuration + splines_orig, _ = tile_creator.create_tile( + parameters=parameters, closure=closure ) - ] - - # Go through all the parameters individually - for i_parameter in range(n_info_per_eval_point): - # Get derivatives w.r.t. one parameter - parameter_sensitivities = np.zeros( - (n_eval_points, n_info_per_eval_point, 1) + # Set evaluation points as 4 random spots in the parametric space + eval_points = np_rng.random( + (n_test_points, splines_orig[0].para_dim) ) - parameter_sensitivities[:, i_parameter, :] = 1 - _, derivatives = tile_creator.create_tile( - parameters=parameters, - parameter_sensitivities=parameter_sensitivities, - ) - deriv_evaluations = [ - deriv.evaluate(eval_points) for deriv in derivatives[0] + splines_orig_evaluations = [ + spl.evaluate(eval_points) for spl in splines_orig ] - # Perform finite difference evaluation - parameters_perturbed = parameters.copy() - parameters_perturbed[:, i_parameter] += heps - splines_perturbed, _ = tile_creator.create_tile( - parameters=parameters_perturbed - ) - spline_perturbed_evaluations = [ - spl.evaluate(eval_points) for spl in splines_perturbed - ] - fd_sensitivities = [ - (spl_pert - spl_orig) / heps - for spl_pert, spl_orig in zip( - spline_perturbed_evaluations, spline_orig_evaluations + # Go through all the parameters individually + for i_parameter in range(n_info_per_eval_point): + # Get derivatives w.r.t. one parameter + parameter_sensitivities = np.zeros( + (n_eval_points, n_info_per_eval_point, 1) ) - ] - # Check every patch - for deriv_orig, deriv_fd in zip( - deriv_evaluations, fd_sensitivities - ): - assert np.allclose(deriv_orig, deriv_fd), ( - "Implemented derivative calculation does not match the derivative " - + "obtained using Finite Differences" + parameter_sensitivities[:, i_parameter, :] = 1 + _, derivatives = tile_creator.create_tile( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + closure=closure, + ) + deriv_evaluations = [ + deriv.evaluate(eval_points) for deriv in derivatives[0] + ] + # Perform finite difference evaluation + parameters_perturbed = parameters.copy() + parameters_perturbed[:, i_parameter] += heps + splines_perturbed, _ = tile_creator.create_tile( + parameters=parameters_perturbed, closure=closure ) + spline_perturbed_evaluations = [ + spl.evaluate(eval_points) for spl in splines_perturbed + ] + # Evaluate finite difference gradient + fd_sensitivities = [ + (spl_pert - spl_orig) / heps + for spl_pert, spl_orig in zip( + spline_perturbed_evaluations, splines_orig_evaluations + ) + ] + # Check every patch + n_patches = len(fd_sensitivities) + for i_patch, deriv_orig, deriv_fd in zip( + range(n_patches), deriv_evaluations, fd_sensitivities + ): + assert np.allclose(deriv_orig, deriv_fd), ( + "Implemented derivative calculation for tile class" + + f"{tile_class}, parameter " + + f"{i_parameter+1}/{n_info_per_eval_point} at patch " + + f"{i_patch+1}/{n_patches} does not match the derivative " + + "obtained using Finite Differences" + ) From b13a3007a3265b6d04cc471545a97aa08d794958 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 27 Aug 2024 11:20:15 +0200 Subject: [PATCH 089/171] Restructure and add derivatives for InverseCross3D tile (WIP) --- .../microstructure/tiles/inverse_cross_3d.py | 1368 +++++++++-------- 1 file changed, 712 insertions(+), 656 deletions(-) diff --git a/splinepy/microstructure/tiles/inverse_cross_3d.py b/splinepy/microstructure/tiles/inverse_cross_3d.py index 6e4f0e010..4af1b2ef6 100644 --- a/splinepy/microstructure/tiles/inverse_cross_3d.py +++ b/splinepy/microstructure/tiles/inverse_cross_3d.py @@ -32,7 +32,8 @@ class InverseCross3D(_TileBase): ] ) _n_info_per_eval_point = 1 - _sensitivities_implemented = False + # TODO: implemented sensitivities are not correct + _sensitivities_implemented = True _closure_directions = ["z_min", "z_max"] _parameter_bounds = [[0.2, 0.3]] * 6 # For default values _parameters_shape = (6, 1) @@ -44,7 +45,7 @@ def _closing_tile( closure=None, boundary_width=0.1, filling_height=0.5, - separator_distance=None, + separator_distance=0.3, **kwargs, # noqa ARG002 ): """Create a closing tile to match with closed surface. @@ -73,10 +74,6 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - # Set default values - if separator_distance is None: - separator_distance = 0.3 - if parameters is None: self._logd("Tile request is not parametrized, setting default 0.2") parameters = ( @@ -893,7 +890,7 @@ def create_tile( self, parameters=None, parameter_sensitivities=None, - separator_distance=None, + separator_distance=0.3, center_expansion=1.0, closure=None, **kwargs, # noqa ARG002 @@ -930,13 +927,6 @@ def create_tile( if not ((center_expansion > 0.5) and (center_expansion < 1.5)): raise ValueError("Center Expansion must be in (.5, 1.5)") - # Set default values - if separator_distance is None: - separator_distance = 0.3 - - if center_expansion is None: - center_expansion = 1.0 - # Check if all radii are in allowed range max_radius = min(0.5, (0.5 / center_expansion)) max_radius = min(max_radius, separator_distance) @@ -953,11 +943,14 @@ def create_tile( ) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) if _np.any(parameters < min_radius) or _np.any( parameters > max_radius @@ -976,644 +969,707 @@ def create_tile( **kwargs, ) - [ - x_min_r, - x_max_r, - y_min_r, - y_max_r, - z_min_r, - z_max_r, - ] = parameters.flatten() - - # center radius - center_r = _np.sum(parameters) / 6.0 * center_expansion - - # Auxiliary values for smooothing (mid-branch thickness) - [ - aux_x_min, - aux_x_max, - aux_y_min, - aux_y_max, - aux_z_min, - aux_z_max, - ] = _np.minimum(parameters.ravel(), center_r) - # Branch midlength - hd_center = 0.5 * (0.5 + center_r) - aux_column_width = 0.5 - 2 * (0.5 - separator_distance) + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + [ + x_min_r, + x_max_r, + y_min_r, + y_max_r, + z_min_r, + z_max_r, + ] = parameters.flatten() + + # center radius + center_r = _np.sum(parameters) / 6.0 * center_expansion + + # Auxiliary values for smooothing (mid-branch thickness) + [ + aux_x_min, + aux_x_max, + aux_y_min, + aux_y_max, + aux_z_min, + aux_z_max, + ] = _np.minimum(parameters.ravel(), center_r) + # Branch midlength + hd_center = 0.5 * (0.5 + center_r) + aux_column_width = 0.5 - 2 * (0.5 - separator_distance) + + v_one_half = 0.5 + center_point = _np.array([0.5, 0.5, 0.5]) + else: + sensitivities_i = parameter_sensitivities[ + :, 0, i_derivative - 1 + ] + [ + x_min_r, + x_max_r, + y_min_r, + y_max_r, + z_min_r, + z_max_r, + ] = sensitivities_i + + center_r = _np.mean(sensitivities_i) * center_expansion + [ + aux_x_min, + aux_x_max, + aux_y_min, + aux_y_max, + aux_z_min, + aux_z_max, + ] = _np.where( + parameters.ravel() + < _np.mean(parameters.ravel()) * center_expansion, + sensitivities_i, + center_r, + ) + # Branch midlength + hd_center = center_r / 2.0 + aux_column_width = 0.0 + v_one_half = 0.0 + center_point = _np.zeros(3) + + # Init return type + spline_list = [] + + # Start with branch interconnections + x_min_y_min = ( + _np.array( + [ + [-v_one_half, -v_one_half, -aux_column_width], + [-separator_distance, -v_one_half, -aux_column_width], + [-y_min_r, -v_one_half, -y_min_r], + [-v_one_half, -separator_distance, -aux_column_width], + [-hd_center, -hd_center, -aux_column_width], + [-aux_y_min, -hd_center, -aux_y_min], + [-v_one_half, -x_min_r, -x_min_r], + [-hd_center, -aux_x_min, -aux_x_min], + [-center_r, -center_r, -center_r], + [-v_one_half, -v_one_half, aux_column_width], + [-separator_distance, -v_one_half, aux_column_width], + [-y_min_r, -v_one_half, y_min_r], + [-v_one_half, -separator_distance, aux_column_width], + [-hd_center, -hd_center, aux_column_width], + [-aux_y_min, -hd_center, aux_y_min], + [-v_one_half, -x_min_r, x_min_r], + [-hd_center, -aux_x_min, aux_x_min], + [-center_r, -center_r, center_r], + ] + ) + + center_point + ) - # Init return type - spline_list = [] + x_max_y_min = ( + _np.array( + [ + [y_min_r, -v_one_half, -y_min_r], + [separator_distance, -v_one_half, -aux_column_width], + [v_one_half, -v_one_half, -aux_column_width], + [aux_y_min, -hd_center, -aux_y_min], + [hd_center, -hd_center, -aux_column_width], + [v_one_half, -separator_distance, -aux_column_width], + [center_r, -center_r, -center_r], + [hd_center, -aux_x_max, -aux_x_max], + [v_one_half, -x_max_r, -x_max_r], + [y_min_r, -v_one_half, y_min_r], + [separator_distance, -v_one_half, aux_column_width], + [v_one_half, -v_one_half, aux_column_width], + [aux_y_min, -hd_center, aux_y_min], + [hd_center, -hd_center, aux_column_width], + [v_one_half, -separator_distance, aux_column_width], + [center_r, -center_r, center_r], + [hd_center, -aux_x_max, aux_x_max], + [v_one_half, -x_max_r, x_max_r], + ] + ) + + center_point + ) + + x_min_y_max = ( + _np.array( + [ + [-v_one_half, x_min_r, -x_min_r], + [-hd_center, aux_x_min, -aux_x_min], + [-center_r, center_r, -center_r], + [-v_one_half, separator_distance, -aux_column_width], + [-hd_center, hd_center, -aux_column_width], + [-aux_y_max, hd_center, -aux_y_max], + [-v_one_half, v_one_half, -aux_column_width], + [-separator_distance, v_one_half, -aux_column_width], + [-y_max_r, v_one_half, -y_max_r], + [-v_one_half, x_min_r, x_min_r], + [-hd_center, aux_x_min, aux_x_min], + [-center_r, center_r, center_r], + [-v_one_half, separator_distance, aux_column_width], + [-hd_center, hd_center, aux_column_width], + [-aux_y_max, hd_center, aux_y_max], + [-v_one_half, v_one_half, aux_column_width], + [-separator_distance, v_one_half, aux_column_width], + [-y_max_r, v_one_half, y_max_r], + ] + ) + + center_point + ) + + x_max_y_max = ( + _np.array( + [ + [center_r, center_r, -center_r], + [hd_center, aux_x_max, -aux_x_max], + [v_one_half, x_max_r, -x_max_r], + [aux_y_max, hd_center, -aux_y_max], + [hd_center, hd_center, -aux_column_width], + [v_one_half, separator_distance, -aux_column_width], + [y_max_r, v_one_half, -y_max_r], + [separator_distance, v_one_half, -aux_column_width], + [v_one_half, v_one_half, -aux_column_width], + [center_r, center_r, center_r], + [hd_center, aux_x_max, aux_x_max], + [v_one_half, x_max_r, x_max_r], + [aux_y_max, hd_center, aux_y_max], + [hd_center, hd_center, aux_column_width], + [v_one_half, separator_distance, aux_column_width], + [y_max_r, v_one_half, y_max_r], + [separator_distance, v_one_half, aux_column_width], + [v_one_half, v_one_half, aux_column_width], + ] + ) + + center_point + ) + + x_min_z_min = ( + _np.array( + [ + [-v_one_half, -aux_column_width, -v_one_half], + [-separator_distance, -aux_column_width, -v_one_half], + [-z_min_r, -z_min_r, -v_one_half], + [-v_one_half, aux_column_width, -v_one_half], + [-separator_distance, aux_column_width, -v_one_half], + [-z_min_r, z_min_r, -v_one_half], + [-v_one_half, -aux_column_width, -separator_distance], + [-hd_center, -aux_column_width, -hd_center], + [-aux_z_min, -aux_z_min, -hd_center], + [-v_one_half, aux_column_width, -separator_distance], + [-hd_center, aux_column_width, -hd_center], + [-aux_z_min, aux_z_min, -hd_center], + [-v_one_half, -x_min_r, -x_min_r], + [-hd_center, -aux_x_min, -aux_x_min], + [-center_r, -center_r, -center_r], + [-v_one_half, x_min_r, -x_min_r], + [-hd_center, aux_x_min, -aux_x_min], + [-center_r, center_r, -center_r], + ] + ) + + center_point + ) + + x_max_z_min = ( + _np.array( + [ + [z_min_r, -z_min_r, -v_one_half], + [separator_distance, -aux_column_width, -v_one_half], + [v_one_half, -aux_column_width, -v_one_half], + [z_min_r, z_min_r, -v_one_half], + [separator_distance, aux_column_width, -v_one_half], + [v_one_half, aux_column_width, -v_one_half], + [aux_z_min, -aux_z_min, -hd_center], + [hd_center, -aux_column_width, -hd_center], + [v_one_half, -aux_column_width, -separator_distance], + [aux_z_min, aux_z_min, -hd_center], + [hd_center, aux_column_width, -hd_center], + [v_one_half, aux_column_width, -separator_distance], + [center_r, -center_r, -center_r], + [hd_center, -aux_x_max, -aux_x_max], + [v_one_half, -x_max_r, -x_max_r], + [center_r, center_r, -center_r], + [hd_center, aux_x_max, -aux_x_max], + [v_one_half, x_max_r, -x_max_r], + ] + ) + + center_point + ) + + x_min_z_max = ( + _np.array( + [ + [-v_one_half, -x_min_r, x_min_r], + [-hd_center, -aux_x_min, aux_x_min], + [-center_r, -center_r, center_r], + [-v_one_half, x_min_r, x_min_r], + [-hd_center, aux_x_min, aux_x_min], + [-center_r, center_r, center_r], + [-v_one_half, -aux_column_width, separator_distance], + [-hd_center, -aux_column_width, hd_center], + [-aux_z_max, -aux_z_max, hd_center], + [-v_one_half, aux_column_width, separator_distance], + [-hd_center, aux_column_width, hd_center], + [-aux_z_max, aux_z_max, hd_center], + [-v_one_half, -aux_column_width, v_one_half], + [-separator_distance, -aux_column_width, v_one_half], + [-z_max_r, -z_max_r, v_one_half], + [-v_one_half, aux_column_width, v_one_half], + [-separator_distance, aux_column_width, v_one_half], + [-z_max_r, z_max_r, v_one_half], + ] + ) + + center_point + ) + + x_max_z_max = ( + _np.array( + [ + [center_r, -center_r, center_r], + [hd_center, -aux_x_max, aux_x_max], + [v_one_half, -x_max_r, x_max_r], + [center_r, center_r, center_r], + [hd_center, aux_x_max, aux_x_max], + [v_one_half, x_max_r, x_max_r], + [aux_z_max, -aux_z_max, hd_center], + [hd_center, -aux_column_width, hd_center], + [v_one_half, -aux_column_width, separator_distance], + [aux_z_max, aux_z_max, hd_center], + [hd_center, aux_column_width, hd_center], + [v_one_half, aux_column_width, separator_distance], + [z_max_r, -z_max_r, v_one_half], + [separator_distance, -aux_column_width, v_one_half], + [v_one_half, -aux_column_width, v_one_half], + [z_max_r, z_max_r, v_one_half], + [separator_distance, aux_column_width, v_one_half], + [v_one_half, aux_column_width, v_one_half], + ] + ) + + center_point + ) + + y_min_z_min = ( + _np.array( + [ + [-aux_column_width, -v_one_half, -v_one_half], + [aux_column_width, -v_one_half, -v_one_half], + [-aux_column_width, -separator_distance, -v_one_half], + [aux_column_width, -separator_distance, -v_one_half], + [-z_min_r, -z_min_r, -v_one_half], + [z_min_r, -z_min_r, -v_one_half], + [-aux_column_width, -v_one_half, -separator_distance], + [aux_column_width, -v_one_half, -separator_distance], + [-aux_column_width, -hd_center, -hd_center], + [aux_column_width, -hd_center, -hd_center], + [-aux_z_min, -aux_z_min, -hd_center], + [aux_z_min, -aux_z_min, -hd_center], + [-y_min_r, -v_one_half, -y_min_r], + [y_min_r, -v_one_half, -y_min_r], + [-aux_y_min, -hd_center, -aux_y_min], + [aux_y_min, -hd_center, -aux_y_min], + [-center_r, -center_r, -center_r], + [center_r, -center_r, -center_r], + ] + ) + + center_point + ) + + y_max_z_min = ( + _np.array( + [ + [-z_min_r, z_min_r, -v_one_half], + [z_min_r, z_min_r, -v_one_half], + [-aux_column_width, separator_distance, -v_one_half], + [aux_column_width, separator_distance, -v_one_half], + [-aux_column_width, v_one_half, -v_one_half], + [aux_column_width, v_one_half, -v_one_half], + [-aux_z_min, aux_z_min, -hd_center], + [aux_z_min, aux_z_min, -hd_center], + [-aux_column_width, hd_center, -hd_center], + [aux_column_width, hd_center, -hd_center], + [-aux_column_width, v_one_half, -separator_distance], + [aux_column_width, v_one_half, -separator_distance], + [-center_r, center_r, -center_r], + [center_r, center_r, -center_r], + [-aux_y_max, hd_center, -aux_y_max], + [aux_y_max, hd_center, -aux_y_max], + [-y_max_r, v_one_half, -y_max_r], + [y_max_r, v_one_half, -y_max_r], + ] + ) + + center_point + ) + + y_min_z_max = ( + _np.array( + [ + [-y_min_r, -v_one_half, y_min_r], + [y_min_r, -v_one_half, y_min_r], + [-aux_y_min, -hd_center, aux_y_min], + [aux_y_min, -hd_center, aux_y_min], + [-center_r, -center_r, center_r], + [center_r, -center_r, center_r], + [-aux_column_width, -v_one_half, separator_distance], + [aux_column_width, -v_one_half, separator_distance], + [-aux_column_width, -hd_center, hd_center], + [aux_column_width, -hd_center, hd_center], + [-aux_z_max, -aux_z_max, hd_center], + [aux_z_max, -aux_z_max, hd_center], + [-aux_column_width, -v_one_half, v_one_half], + [aux_column_width, -v_one_half, v_one_half], + [-aux_column_width, -separator_distance, v_one_half], + [aux_column_width, -separator_distance, v_one_half], + [-z_max_r, -z_max_r, v_one_half], + [z_max_r, -z_max_r, v_one_half], + ] + ) + + center_point + ) + + y_max_z_max = ( + _np.array( + [ + [-center_r, center_r, center_r], + [center_r, center_r, center_r], + [-aux_y_max, hd_center, aux_y_max], + [aux_y_max, hd_center, aux_y_max], + [-y_max_r, v_one_half, y_max_r], + [y_max_r, v_one_half, y_max_r], + [-aux_z_max, aux_z_max, hd_center], + [aux_z_max, aux_z_max, hd_center], + [-aux_column_width, hd_center, hd_center], + [aux_column_width, hd_center, hd_center], + [-aux_column_width, v_one_half, separator_distance], + [aux_column_width, v_one_half, separator_distance], + [-z_max_r, z_max_r, v_one_half], + [z_max_r, z_max_r, v_one_half], + [-aux_column_width, separator_distance, v_one_half], + [aux_column_width, separator_distance, v_one_half], + [-aux_column_width, v_one_half, v_one_half], + [aux_column_width, v_one_half, v_one_half], + ] + ) + + center_point + ) + + x_min_y_min_z_min = ( + _np.array( + [ + [-v_one_half, -v_one_half, -v_one_half], + [-separator_distance, -v_one_half, -v_one_half], + [-aux_column_width, -v_one_half, -v_one_half], + [-v_one_half, -separator_distance, -v_one_half], + [ + -separator_distance, + -separator_distance, + -v_one_half, + ], + [-aux_column_width, -separator_distance, -v_one_half], + [-v_one_half, -aux_column_width, -v_one_half], + [-separator_distance, -aux_column_width, -v_one_half], + [-z_min_r, -z_min_r, -v_one_half], + [-v_one_half, -v_one_half, -separator_distance], + [ + -separator_distance, + -v_one_half, + -separator_distance, + ], + [-aux_column_width, -v_one_half, -separator_distance], + [ + -v_one_half, + -separator_distance, + -separator_distance, + ], + [-hd_center, -hd_center, -hd_center], + [-aux_column_width, -hd_center, -hd_center], + [-v_one_half, -aux_column_width, -separator_distance], + [-hd_center, -aux_column_width, -hd_center], + [-aux_z_min, -aux_z_min, -hd_center], + [-v_one_half, -v_one_half, -aux_column_width], + [-separator_distance, -v_one_half, -aux_column_width], + [-y_min_r, -v_one_half, -y_min_r], + [-v_one_half, -separator_distance, -aux_column_width], + [-hd_center, -hd_center, -aux_column_width], + [-aux_y_min, -hd_center, -aux_y_min], + [-v_one_half, -x_min_r, -x_min_r], + [-hd_center, -aux_x_min, -aux_x_min], + [-center_r, -center_r, -center_r], + ] + ) + + center_point + ) + + x_max_y_min_z_min = ( + _np.array( + [ + [aux_column_width, -v_one_half, -v_one_half], + [separator_distance, -v_one_half, -v_one_half], + [v_one_half, -v_one_half, -v_one_half], + [aux_column_width, -separator_distance, -v_one_half], + [separator_distance, -separator_distance, -v_one_half], + [v_one_half, -separator_distance, -v_one_half], + [z_min_r, -z_min_r, -v_one_half], + [separator_distance, -aux_column_width, -v_one_half], + [v_one_half, -aux_column_width, -v_one_half], + [aux_column_width, -v_one_half, -separator_distance], + [separator_distance, -v_one_half, -separator_distance], + [v_one_half, -v_one_half, -separator_distance], + [aux_column_width, -hd_center, -hd_center], + [hd_center, -hd_center, -hd_center], + [v_one_half, -separator_distance, -separator_distance], + [aux_z_min, -aux_z_min, -hd_center], + [hd_center, -aux_column_width, -hd_center], + [v_one_half, -aux_column_width, -separator_distance], + [y_min_r, -v_one_half, -y_min_r], + [separator_distance, -v_one_half, -aux_column_width], + [v_one_half, -v_one_half, -aux_column_width], + [aux_y_min, -hd_center, -aux_y_min], + [hd_center, -hd_center, -aux_column_width], + [v_one_half, -separator_distance, -aux_column_width], + [center_r, -center_r, -center_r], + [hd_center, -aux_x_max, -aux_x_max], + [v_one_half, -x_max_r, -x_max_r], + ] + ) + + center_point + ) + + x_min_y_max_z_min = ( + _np.array( + [ + [-v_one_half, aux_column_width, -v_one_half], + [-separator_distance, aux_column_width, -v_one_half], + [-z_min_r, z_min_r, -v_one_half], + [-v_one_half, separator_distance, -v_one_half], + [-separator_distance, separator_distance, -v_one_half], + [-aux_column_width, separator_distance, -v_one_half], + [-v_one_half, v_one_half, -v_one_half], + [-separator_distance, v_one_half, -v_one_half], + [-aux_column_width, v_one_half, -v_one_half], + [-v_one_half, aux_column_width, -separator_distance], + [-hd_center, aux_column_width, -hd_center], + [-aux_z_min, aux_z_min, -hd_center], + [-v_one_half, separator_distance, -separator_distance], + [-hd_center, hd_center, -hd_center], + [-aux_column_width, hd_center, -hd_center], + [-v_one_half, v_one_half, -separator_distance], + [-separator_distance, v_one_half, -separator_distance], + [-aux_column_width, v_one_half, -separator_distance], + [-v_one_half, x_min_r, -x_min_r], + [-hd_center, aux_x_min, -aux_x_min], + [-center_r, center_r, -center_r], + [-v_one_half, separator_distance, -aux_column_width], + [-hd_center, hd_center, -aux_column_width], + [-aux_y_max, hd_center, -aux_y_max], + [-v_one_half, v_one_half, -aux_column_width], + [-separator_distance, v_one_half, -aux_column_width], + [-y_max_r, v_one_half, -y_max_r], + ] + ) + + center_point + ) + + x_max_y_max_z_min = ( + _np.array( + [ + [z_min_r, z_min_r, -v_one_half], + [separator_distance, aux_column_width, -v_one_half], + [v_one_half, aux_column_width, -v_one_half], + [aux_column_width, separator_distance, -v_one_half], + [separator_distance, separator_distance, -v_one_half], + [v_one_half, separator_distance, -v_one_half], + [aux_column_width, v_one_half, -v_one_half], + [separator_distance, v_one_half, -v_one_half], + [v_one_half, v_one_half, -v_one_half], + [aux_z_min, aux_z_min, -hd_center], + [hd_center, aux_column_width, -hd_center], + [v_one_half, aux_column_width, -separator_distance], + [aux_column_width, hd_center, -hd_center], + [hd_center, hd_center, -hd_center], + [v_one_half, separator_distance, -separator_distance], + [aux_column_width, v_one_half, -separator_distance], + [separator_distance, v_one_half, -separator_distance], + [v_one_half, v_one_half, -separator_distance], + [center_r, center_r, -center_r], + [hd_center, aux_x_max, -aux_x_max], + [v_one_half, x_max_r, -x_max_r], + [aux_y_max, hd_center, -aux_y_max], + [hd_center, hd_center, -aux_column_width], + [v_one_half, separator_distance, -aux_column_width], + [y_max_r, v_one_half, -y_max_r], + [separator_distance, v_one_half, -aux_column_width], + [v_one_half, v_one_half, -aux_column_width], + ] + ) + + center_point + ) + + x_min_y_min_z_max = ( + _np.array( + [ + [-v_one_half, -v_one_half, aux_column_width], + [-separator_distance, -v_one_half, aux_column_width], + [-y_min_r, -v_one_half, y_min_r], + [-v_one_half, -separator_distance, aux_column_width], + [-hd_center, -hd_center, aux_column_width], + [-aux_y_min, -hd_center, aux_y_min], + [-v_one_half, -x_min_r, x_min_r], + [-hd_center, -aux_x_min, aux_x_min], + [-center_r, -center_r, center_r], + [-v_one_half, -v_one_half, separator_distance], + [-separator_distance, -v_one_half, separator_distance], + [-aux_column_width, -v_one_half, separator_distance], + [-v_one_half, -separator_distance, separator_distance], + [-hd_center, -hd_center, hd_center], + [-aux_column_width, -hd_center, hd_center], + [-v_one_half, -aux_column_width, separator_distance], + [-hd_center, -aux_column_width, hd_center], + [-aux_z_max, -aux_z_max, hd_center], + [-v_one_half, -v_one_half, v_one_half], + [-separator_distance, -v_one_half, v_one_half], + [-aux_column_width, -v_one_half, v_one_half], + [-v_one_half, -separator_distance, v_one_half], + [-separator_distance, -separator_distance, v_one_half], + [-aux_column_width, -separator_distance, v_one_half], + [-v_one_half, -aux_column_width, v_one_half], + [-separator_distance, -aux_column_width, v_one_half], + [-z_max_r, -z_max_r, v_one_half], + ] + ) + + center_point + ) + + x_max_y_min_z_max = ( + _np.array( + [ + [y_min_r, -v_one_half, y_min_r], + [separator_distance, -v_one_half, aux_column_width], + [v_one_half, -v_one_half, aux_column_width], + [aux_y_min, -hd_center, aux_y_min], + [hd_center, -hd_center, aux_column_width], + [v_one_half, -separator_distance, aux_column_width], + [center_r, -center_r, center_r], + [hd_center, -aux_x_max, aux_x_max], + [v_one_half, -x_max_r, x_max_r], + [aux_column_width, -v_one_half, separator_distance], + [separator_distance, -v_one_half, separator_distance], + [v_one_half, -v_one_half, separator_distance], + [aux_column_width, -hd_center, hd_center], + [hd_center, -hd_center, hd_center], + [v_one_half, -separator_distance, separator_distance], + [aux_z_max, -aux_z_max, hd_center], + [hd_center, -aux_column_width, hd_center], + [v_one_half, -aux_column_width, separator_distance], + [aux_column_width, -v_one_half, v_one_half], + [separator_distance, -v_one_half, v_one_half], + [v_one_half, -v_one_half, v_one_half], + [aux_column_width, -separator_distance, v_one_half], + [separator_distance, -separator_distance, v_one_half], + [v_one_half, -separator_distance, v_one_half], + [z_max_r, -z_max_r, v_one_half], + [separator_distance, -aux_column_width, v_one_half], + [v_one_half, -aux_column_width, v_one_half], + ] + ) + + center_point + ) + + x_min_y_max_z_max = ( + _np.array( + [ + [-v_one_half, x_min_r, x_min_r], + [-hd_center, aux_x_min, aux_x_min], + [-center_r, center_r, center_r], + [-v_one_half, separator_distance, aux_column_width], + [-hd_center, hd_center, aux_column_width], + [-aux_y_max, hd_center, aux_y_max], + [-v_one_half, v_one_half, aux_column_width], + [-separator_distance, v_one_half, aux_column_width], + [-y_max_r, v_one_half, y_max_r], + [-v_one_half, aux_column_width, separator_distance], + [-hd_center, aux_column_width, hd_center], + [-aux_z_max, aux_z_max, hd_center], + [-v_one_half, separator_distance, separator_distance], + [-hd_center, hd_center, hd_center], + [-aux_column_width, hd_center, hd_center], + [-v_one_half, v_one_half, separator_distance], + [-separator_distance, v_one_half, separator_distance], + [-aux_column_width, v_one_half, separator_distance], + [-v_one_half, aux_column_width, v_one_half], + [-separator_distance, aux_column_width, v_one_half], + [-z_max_r, z_max_r, v_one_half], + [-v_one_half, separator_distance, v_one_half], + [-separator_distance, separator_distance, v_one_half], + [-aux_column_width, separator_distance, v_one_half], + [-v_one_half, v_one_half, v_one_half], + [-separator_distance, v_one_half, v_one_half], + [-aux_column_width, v_one_half, v_one_half], + ] + ) + + center_point + ) + + x_max_y_max_z_max = ( + _np.array( + [ + [center_r, center_r, center_r], + [hd_center, aux_x_max, aux_x_max], + [v_one_half, x_max_r, x_max_r], + [aux_y_max, hd_center, aux_y_max], + [hd_center, hd_center, aux_column_width], + [v_one_half, separator_distance, aux_column_width], + [y_max_r, v_one_half, y_max_r], + [separator_distance, v_one_half, aux_column_width], + [v_one_half, v_one_half, aux_column_width], + [aux_z_max, aux_z_max, hd_center], + [hd_center, aux_column_width, hd_center], + [v_one_half, aux_column_width, separator_distance], + [aux_column_width, hd_center, hd_center], + [hd_center, hd_center, hd_center], + [v_one_half, separator_distance, separator_distance], + [aux_column_width, v_one_half, separator_distance], + [separator_distance, v_one_half, separator_distance], + [v_one_half, v_one_half, separator_distance], + [z_max_r, z_max_r, v_one_half], + [separator_distance, aux_column_width, v_one_half], + [v_one_half, aux_column_width, v_one_half], + [aux_column_width, separator_distance, v_one_half], + [separator_distance, separator_distance, v_one_half], + [v_one_half, separator_distance, v_one_half], + [aux_column_width, v_one_half, v_one_half], + [separator_distance, v_one_half, v_one_half], + [v_one_half, v_one_half, v_one_half], + ] + ) + + center_point + ) + + # Append the control points to the spline list + for control_points, degrees in [ + (x_min_y_min, [2, 2, 1]), + (x_max_y_min, [2, 2, 1]), + (x_min_y_max, [2, 2, 1]), + (x_max_y_max, [2, 2, 1]), + (x_min_z_min, [2, 1, 2]), + (x_max_z_min, [2, 1, 2]), + (x_min_z_max, [2, 1, 2]), + (x_max_z_max, [2, 1, 2]), + (y_min_z_min, [1, 2, 2]), + (y_max_z_min, [1, 2, 2]), + (y_min_z_max, [1, 2, 2]), + (y_max_z_max, [1, 2, 2]), + (x_min_y_min_z_min, [2, 2, 2]), + (x_max_y_min_z_min, [2, 2, 2]), + (x_min_y_max_z_min, [2, 2, 2]), + (x_max_y_max_z_min, [2, 2, 2]), + (x_min_y_min_z_max, [2, 2, 2]), + (x_max_y_min_z_max, [2, 2, 2]), + (x_min_y_max_z_max, [2, 2, 2]), + (x_max_y_max_z_max, [2, 2, 2]), + ]: + spline_list.append( + _Bezier(degrees=degrees, control_points=control_points) + ) + + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) - # Start with branch interconnections - x_min_y_min = _np.array( - [ - [-0.5, -0.5, -aux_column_width], - [-separator_distance, -0.5, -aux_column_width], - [-y_min_r, -0.5, -y_min_r], - [-0.5, -separator_distance, -aux_column_width], - [-hd_center, -hd_center, -aux_column_width], - [-aux_y_min, -hd_center, -aux_y_min], - [-0.5, -x_min_r, -x_min_r], - [-hd_center, -aux_x_min, -aux_x_min], - [-center_r, -center_r, -center_r], - [-0.5, -0.5, aux_column_width], - [-separator_distance, -0.5, aux_column_width], - [-y_min_r, -0.5, y_min_r], - [-0.5, -separator_distance, aux_column_width], - [-hd_center, -hd_center, aux_column_width], - [-aux_y_min, -hd_center, aux_y_min], - [-0.5, -x_min_r, x_min_r], - [-hd_center, -aux_x_min, aux_x_min], - [-center_r, -center_r, center_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 1], control_points=x_min_y_min) - ) - - x_max_y_min = _np.array( - [ - [y_min_r, -0.5, -y_min_r], - [separator_distance, -0.5, -aux_column_width], - [0.5, -0.5, -aux_column_width], - [aux_y_min, -hd_center, -aux_y_min], - [hd_center, -hd_center, -aux_column_width], - [0.5, -separator_distance, -aux_column_width], - [center_r, -center_r, -center_r], - [hd_center, -aux_x_max, -aux_x_max], - [0.5, -x_max_r, -x_max_r], - [y_min_r, -0.5, y_min_r], - [separator_distance, -0.5, aux_column_width], - [0.5, -0.5, aux_column_width], - [aux_y_min, -hd_center, aux_y_min], - [hd_center, -hd_center, aux_column_width], - [0.5, -separator_distance, aux_column_width], - [center_r, -center_r, center_r], - [hd_center, -aux_x_max, aux_x_max], - [0.5, -x_max_r, x_max_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 1], control_points=x_max_y_min) - ) - - x_min_y_max = _np.array( - [ - [-0.5, x_min_r, -x_min_r], - [-hd_center, aux_x_min, -aux_x_min], - [-center_r, center_r, -center_r], - [-0.5, separator_distance, -aux_column_width], - [-hd_center, hd_center, -aux_column_width], - [-aux_y_max, hd_center, -aux_y_max], - [-0.5, 0.5, -aux_column_width], - [-separator_distance, 0.5, -aux_column_width], - [-y_max_r, 0.5, -y_max_r], - [-0.5, x_min_r, x_min_r], - [-hd_center, aux_x_min, aux_x_min], - [-center_r, center_r, center_r], - [-0.5, separator_distance, aux_column_width], - [-hd_center, hd_center, aux_column_width], - [-aux_y_max, hd_center, aux_y_max], - [-0.5, 0.5, aux_column_width], - [-separator_distance, 0.5, aux_column_width], - [-y_max_r, 0.5, y_max_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 1], control_points=x_min_y_max) - ) - - x_max_y_max = _np.array( - [ - [center_r, center_r, -center_r], - [hd_center, aux_x_max, -aux_x_max], - [0.5, x_max_r, -x_max_r], - [aux_y_max, hd_center, -aux_y_max], - [hd_center, hd_center, -aux_column_width], - [0.5, separator_distance, -aux_column_width], - [y_max_r, 0.5, -y_max_r], - [separator_distance, 0.5, -aux_column_width], - [0.5, 0.5, -aux_column_width], - [center_r, center_r, center_r], - [hd_center, aux_x_max, aux_x_max], - [0.5, x_max_r, x_max_r], - [aux_y_max, hd_center, aux_y_max], - [hd_center, hd_center, aux_column_width], - [0.5, separator_distance, aux_column_width], - [y_max_r, 0.5, y_max_r], - [separator_distance, 0.5, aux_column_width], - [0.5, 0.5, aux_column_width], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 1], control_points=x_max_y_max) - ) - - x_min_z_min = _np.array( - [ - [-0.5, -aux_column_width, -0.5], - [-separator_distance, -aux_column_width, -0.5], - [-z_min_r, -z_min_r, -0.5], - [-0.5, aux_column_width, -0.5], - [-separator_distance, aux_column_width, -0.5], - [-z_min_r, z_min_r, -0.5], - [-0.5, -aux_column_width, -separator_distance], - [-hd_center, -aux_column_width, -hd_center], - [-aux_z_min, -aux_z_min, -hd_center], - [-0.5, aux_column_width, -separator_distance], - [-hd_center, aux_column_width, -hd_center], - [-aux_z_min, aux_z_min, -hd_center], - [-0.5, -x_min_r, -x_min_r], - [-hd_center, -aux_x_min, -aux_x_min], - [-center_r, -center_r, -center_r], - [-0.5, x_min_r, -x_min_r], - [-hd_center, aux_x_min, -aux_x_min], - [-center_r, center_r, -center_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 1, 2], control_points=x_min_z_min) - ) - - x_max_z_min = _np.array( - [ - [z_min_r, -z_min_r, -0.5], - [separator_distance, -aux_column_width, -0.5], - [0.5, -aux_column_width, -0.5], - [z_min_r, z_min_r, -0.5], - [separator_distance, aux_column_width, -0.5], - [0.5, aux_column_width, -0.5], - [aux_z_min, -aux_z_min, -hd_center], - [hd_center, -aux_column_width, -hd_center], - [0.5, -aux_column_width, -separator_distance], - [aux_z_min, aux_z_min, -hd_center], - [hd_center, aux_column_width, -hd_center], - [0.5, aux_column_width, -separator_distance], - [center_r, -center_r, -center_r], - [hd_center, -aux_x_max, -aux_x_max], - [0.5, -x_max_r, -x_max_r], - [center_r, center_r, -center_r], - [hd_center, aux_x_max, -aux_x_max], - [0.5, x_max_r, -x_max_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 1, 2], control_points=x_max_z_min) - ) - - x_min_z_max = _np.array( - [ - [-0.5, -x_min_r, x_min_r], - [-hd_center, -aux_x_min, aux_x_min], - [-center_r, -center_r, center_r], - [-0.5, x_min_r, x_min_r], - [-hd_center, aux_x_min, aux_x_min], - [-center_r, center_r, center_r], - [-0.5, -aux_column_width, separator_distance], - [-hd_center, -aux_column_width, hd_center], - [-aux_z_max, -aux_z_max, hd_center], - [-0.5, aux_column_width, separator_distance], - [-hd_center, aux_column_width, hd_center], - [-aux_z_max, aux_z_max, hd_center], - [-0.5, -aux_column_width, 0.5], - [-separator_distance, -aux_column_width, 0.5], - [-z_max_r, -z_max_r, 0.5], - [-0.5, aux_column_width, 0.5], - [-separator_distance, aux_column_width, 0.5], - [-z_max_r, z_max_r, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 1, 2], control_points=x_min_z_max) - ) - - x_max_z_max = _np.array( - [ - [center_r, -center_r, center_r], - [hd_center, -aux_x_max, aux_x_max], - [0.5, -x_max_r, x_max_r], - [center_r, center_r, center_r], - [hd_center, aux_x_max, aux_x_max], - [0.5, x_max_r, x_max_r], - [aux_z_max, -aux_z_max, hd_center], - [hd_center, -aux_column_width, hd_center], - [0.5, -aux_column_width, separator_distance], - [aux_z_max, aux_z_max, hd_center], - [hd_center, aux_column_width, hd_center], - [0.5, aux_column_width, separator_distance], - [z_max_r, -z_max_r, 0.5], - [separator_distance, -aux_column_width, 0.5], - [0.5, -aux_column_width, 0.5], - [z_max_r, z_max_r, 0.5], - [separator_distance, aux_column_width, 0.5], - [0.5, aux_column_width, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 1, 2], control_points=x_max_z_max) - ) - - y_min_z_min = _np.array( - [ - [-aux_column_width, -0.5, -0.5], - [aux_column_width, -0.5, -0.5], - [-aux_column_width, -separator_distance, -0.5], - [aux_column_width, -separator_distance, -0.5], - [-z_min_r, -z_min_r, -0.5], - [z_min_r, -z_min_r, -0.5], - [-aux_column_width, -0.5, -separator_distance], - [aux_column_width, -0.5, -separator_distance], - [-aux_column_width, -hd_center, -hd_center], - [aux_column_width, -hd_center, -hd_center], - [-aux_z_min, -aux_z_min, -hd_center], - [aux_z_min, -aux_z_min, -hd_center], - [-y_min_r, -0.5, -y_min_r], - [y_min_r, -0.5, -y_min_r], - [-aux_y_min, -hd_center, -aux_y_min], - [aux_y_min, -hd_center, -aux_y_min], - [-center_r, -center_r, -center_r], - [center_r, -center_r, -center_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[1, 2, 2], control_points=y_min_z_min) - ) - - y_max_z_min = _np.array( - [ - [-z_min_r, z_min_r, -0.5], - [z_min_r, z_min_r, -0.5], - [-aux_column_width, separator_distance, -0.5], - [aux_column_width, separator_distance, -0.5], - [-aux_column_width, 0.5, -0.5], - [aux_column_width, 0.5, -0.5], - [-aux_z_min, aux_z_min, -hd_center], - [aux_z_min, aux_z_min, -hd_center], - [-aux_column_width, hd_center, -hd_center], - [aux_column_width, hd_center, -hd_center], - [-aux_column_width, 0.5, -separator_distance], - [aux_column_width, 0.5, -separator_distance], - [-center_r, center_r, -center_r], - [center_r, center_r, -center_r], - [-aux_y_max, hd_center, -aux_y_max], - [aux_y_max, hd_center, -aux_y_max], - [-y_max_r, 0.5, -y_max_r], - [y_max_r, 0.5, -y_max_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[1, 2, 2], control_points=y_max_z_min) - ) - - y_min_z_max = _np.array( - [ - [-y_min_r, -0.5, y_min_r], - [y_min_r, -0.5, y_min_r], - [-aux_y_min, -hd_center, aux_y_min], - [aux_y_min, -hd_center, aux_y_min], - [-center_r, -center_r, center_r], - [center_r, -center_r, center_r], - [-aux_column_width, -0.5, separator_distance], - [aux_column_width, -0.5, separator_distance], - [-aux_column_width, -hd_center, hd_center], - [aux_column_width, -hd_center, hd_center], - [-aux_z_max, -aux_z_max, hd_center], - [aux_z_max, -aux_z_max, hd_center], - [-aux_column_width, -0.5, 0.5], - [aux_column_width, -0.5, 0.5], - [-aux_column_width, -separator_distance, 0.5], - [aux_column_width, -separator_distance, 0.5], - [-z_max_r, -z_max_r, 0.5], - [z_max_r, -z_max_r, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[1, 2, 2], control_points=y_min_z_max) - ) - - y_max_z_max = _np.array( - [ - [-center_r, center_r, center_r], - [center_r, center_r, center_r], - [-aux_y_max, hd_center, aux_y_max], - [aux_y_max, hd_center, aux_y_max], - [-y_max_r, 0.5, y_max_r], - [y_max_r, 0.5, y_max_r], - [-aux_z_max, aux_z_max, hd_center], - [aux_z_max, aux_z_max, hd_center], - [-aux_column_width, hd_center, hd_center], - [aux_column_width, hd_center, hd_center], - [-aux_column_width, 0.5, separator_distance], - [aux_column_width, 0.5, separator_distance], - [-z_max_r, z_max_r, 0.5], - [z_max_r, z_max_r, 0.5], - [-aux_column_width, separator_distance, 0.5], - [aux_column_width, separator_distance, 0.5], - [-aux_column_width, 0.5, 0.5], - [aux_column_width, 0.5, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[1, 2, 2], control_points=y_max_z_max) - ) - - x_min_y_min_z_min = _np.array( - [ - [-0.5, -0.5, -0.5], - [-separator_distance, -0.5, -0.5], - [-aux_column_width, -0.5, -0.5], - [-0.5, -separator_distance, -0.5], - [-separator_distance, -separator_distance, -0.5], - [-aux_column_width, -separator_distance, -0.5], - [-0.5, -aux_column_width, -0.5], - [-separator_distance, -aux_column_width, -0.5], - [-z_min_r, -z_min_r, -0.5], - [-0.5, -0.5, -separator_distance], - [-separator_distance, -0.5, -separator_distance], - [-aux_column_width, -0.5, -separator_distance], - [-0.5, -separator_distance, -separator_distance], - [-hd_center, -hd_center, -hd_center], - [-aux_column_width, -hd_center, -hd_center], - [-0.5, -aux_column_width, -separator_distance], - [-hd_center, -aux_column_width, -hd_center], - [-aux_z_min, -aux_z_min, -hd_center], - [-0.5, -0.5, -aux_column_width], - [-separator_distance, -0.5, -aux_column_width], - [-y_min_r, -0.5, -y_min_r], - [-0.5, -separator_distance, -aux_column_width], - [-hd_center, -hd_center, -aux_column_width], - [-aux_y_min, -hd_center, -aux_y_min], - [-0.5, -x_min_r, -x_min_r], - [-hd_center, -aux_x_min, -aux_x_min], - [-center_r, -center_r, -center_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_min_y_min_z_min) - ) - - x_max_y_min_z_min = _np.array( - [ - [aux_column_width, -0.5, -0.5], - [separator_distance, -0.5, -0.5], - [0.5, -0.5, -0.5], - [aux_column_width, -separator_distance, -0.5], - [separator_distance, -separator_distance, -0.5], - [0.5, -separator_distance, -0.5], - [z_min_r, -z_min_r, -0.5], - [separator_distance, -aux_column_width, -0.5], - [0.5, -aux_column_width, -0.5], - [aux_column_width, -0.5, -separator_distance], - [separator_distance, -0.5, -separator_distance], - [0.5, -0.5, -separator_distance], - [aux_column_width, -hd_center, -hd_center], - [hd_center, -hd_center, -hd_center], - [0.5, -separator_distance, -separator_distance], - [aux_z_min, -aux_z_min, -hd_center], - [hd_center, -aux_column_width, -hd_center], - [0.5, -aux_column_width, -separator_distance], - [y_min_r, -0.5, -y_min_r], - [separator_distance, -0.5, -aux_column_width], - [0.5, -0.5, -aux_column_width], - [aux_y_min, -hd_center, -aux_y_min], - [hd_center, -hd_center, -aux_column_width], - [0.5, -separator_distance, -aux_column_width], - [center_r, -center_r, -center_r], - [hd_center, -aux_x_max, -aux_x_max], - [0.5, -x_max_r, -x_max_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_max_y_min_z_min) - ) - - x_min_y_max_z_min = _np.array( - [ - [-0.5, aux_column_width, -0.5], - [-separator_distance, aux_column_width, -0.5], - [-z_min_r, z_min_r, -0.5], - [-0.5, separator_distance, -0.5], - [-separator_distance, separator_distance, -0.5], - [-aux_column_width, separator_distance, -0.5], - [-0.5, 0.5, -0.5], - [-separator_distance, 0.5, -0.5], - [-aux_column_width, 0.5, -0.5], - [-0.5, aux_column_width, -separator_distance], - [-hd_center, aux_column_width, -hd_center], - [-aux_z_min, aux_z_min, -hd_center], - [-0.5, separator_distance, -separator_distance], - [-hd_center, hd_center, -hd_center], - [-aux_column_width, hd_center, -hd_center], - [-0.5, 0.5, -separator_distance], - [-separator_distance, 0.5, -separator_distance], - [-aux_column_width, 0.5, -separator_distance], - [-0.5, x_min_r, -x_min_r], - [-hd_center, aux_x_min, -aux_x_min], - [-center_r, center_r, -center_r], - [-0.5, separator_distance, -aux_column_width], - [-hd_center, hd_center, -aux_column_width], - [-aux_y_max, hd_center, -aux_y_max], - [-0.5, 0.5, -aux_column_width], - [-separator_distance, 0.5, -aux_column_width], - [-y_max_r, 0.5, -y_max_r], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_min_y_max_z_min) - ) - - x_max_y_max_z_min = _np.array( - [ - [z_min_r, z_min_r, -0.5], - [separator_distance, aux_column_width, -0.5], - [0.5, aux_column_width, -0.5], - [aux_column_width, separator_distance, -0.5], - [separator_distance, separator_distance, -0.5], - [0.5, separator_distance, -0.5], - [aux_column_width, 0.5, -0.5], - [separator_distance, 0.5, -0.5], - [0.5, 0.5, -0.5], - [aux_z_min, aux_z_min, -hd_center], - [hd_center, aux_column_width, -hd_center], - [0.5, aux_column_width, -separator_distance], - [aux_column_width, hd_center, -hd_center], - [hd_center, hd_center, -hd_center], - [0.5, separator_distance, -separator_distance], - [aux_column_width, 0.5, -separator_distance], - [separator_distance, 0.5, -separator_distance], - [0.5, 0.5, -separator_distance], - [center_r, center_r, -center_r], - [hd_center, aux_x_max, -aux_x_max], - [0.5, x_max_r, -x_max_r], - [aux_y_max, hd_center, -aux_y_max], - [hd_center, hd_center, -aux_column_width], - [0.5, separator_distance, -aux_column_width], - [y_max_r, 0.5, -y_max_r], - [separator_distance, 0.5, -aux_column_width], - [0.5, 0.5, -aux_column_width], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_max_y_max_z_min) - ) - - x_min_y_min_z_max = _np.array( - [ - [-0.5, -0.5, aux_column_width], - [-separator_distance, -0.5, aux_column_width], - [-y_min_r, -0.5, y_min_r], - [-0.5, -separator_distance, aux_column_width], - [-hd_center, -hd_center, aux_column_width], - [-aux_y_min, -hd_center, aux_y_min], - [-0.5, -x_min_r, x_min_r], - [-hd_center, -aux_x_min, aux_x_min], - [-center_r, -center_r, center_r], - [-0.5, -0.5, separator_distance], - [-separator_distance, -0.5, separator_distance], - [-aux_column_width, -0.5, separator_distance], - [-0.5, -separator_distance, separator_distance], - [-hd_center, -hd_center, hd_center], - [-aux_column_width, -hd_center, hd_center], - [-0.5, -aux_column_width, separator_distance], - [-hd_center, -aux_column_width, hd_center], - [-aux_z_max, -aux_z_max, hd_center], - [-0.5, -0.5, 0.5], - [-separator_distance, -0.5, 0.5], - [-aux_column_width, -0.5, 0.5], - [-0.5, -separator_distance, 0.5], - [-separator_distance, -separator_distance, 0.5], - [-aux_column_width, -separator_distance, 0.5], - [-0.5, -aux_column_width, 0.5], - [-separator_distance, -aux_column_width, 0.5], - [-z_max_r, -z_max_r, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_min_y_min_z_max) - ) - - x_max_y_min_z_max = _np.array( - [ - [y_min_r, -0.5, y_min_r], - [separator_distance, -0.5, aux_column_width], - [0.5, -0.5, aux_column_width], - [aux_y_min, -hd_center, aux_y_min], - [hd_center, -hd_center, aux_column_width], - [0.5, -separator_distance, aux_column_width], - [center_r, -center_r, center_r], - [hd_center, -aux_x_max, aux_x_max], - [0.5, -x_max_r, x_max_r], - [aux_column_width, -0.5, separator_distance], - [separator_distance, -0.5, separator_distance], - [0.5, -0.5, separator_distance], - [aux_column_width, -hd_center, hd_center], - [hd_center, -hd_center, hd_center], - [0.5, -separator_distance, separator_distance], - [aux_z_max, -aux_z_max, hd_center], - [hd_center, -aux_column_width, hd_center], - [0.5, -aux_column_width, separator_distance], - [aux_column_width, -0.5, 0.5], - [separator_distance, -0.5, 0.5], - [0.5, -0.5, 0.5], - [aux_column_width, -separator_distance, 0.5], - [separator_distance, -separator_distance, 0.5], - [0.5, -separator_distance, 0.5], - [z_max_r, -z_max_r, 0.5], - [separator_distance, -aux_column_width, 0.5], - [0.5, -aux_column_width, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_max_y_min_z_max) - ) - - x_min_y_max_z_max = _np.array( - [ - [-0.5, x_min_r, x_min_r], - [-hd_center, aux_x_min, aux_x_min], - [-center_r, center_r, center_r], - [-0.5, separator_distance, aux_column_width], - [-hd_center, hd_center, aux_column_width], - [-aux_y_max, hd_center, aux_y_max], - [-0.5, 0.5, aux_column_width], - [-separator_distance, 0.5, aux_column_width], - [-y_max_r, 0.5, y_max_r], - [-0.5, aux_column_width, separator_distance], - [-hd_center, aux_column_width, hd_center], - [-aux_z_max, aux_z_max, hd_center], - [-0.5, separator_distance, separator_distance], - [-hd_center, hd_center, hd_center], - [-aux_column_width, hd_center, hd_center], - [-0.5, 0.5, separator_distance], - [-separator_distance, 0.5, separator_distance], - [-aux_column_width, 0.5, separator_distance], - [-0.5, aux_column_width, 0.5], - [-separator_distance, aux_column_width, 0.5], - [-z_max_r, z_max_r, 0.5], - [-0.5, separator_distance, 0.5], - [-separator_distance, separator_distance, 0.5], - [-aux_column_width, separator_distance, 0.5], - [-0.5, 0.5, 0.5], - [-separator_distance, 0.5, 0.5], - [-aux_column_width, 0.5, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_min_y_max_z_max) - ) - - x_max_y_max_z_max = _np.array( - [ - [center_r, center_r, center_r], - [hd_center, aux_x_max, aux_x_max], - [0.5, x_max_r, x_max_r], - [aux_y_max, hd_center, aux_y_max], - [hd_center, hd_center, aux_column_width], - [0.5, separator_distance, aux_column_width], - [y_max_r, 0.5, y_max_r], - [separator_distance, 0.5, aux_column_width], - [0.5, 0.5, aux_column_width], - [aux_z_max, aux_z_max, hd_center], - [hd_center, aux_column_width, hd_center], - [0.5, aux_column_width, separator_distance], - [aux_column_width, hd_center, hd_center], - [hd_center, hd_center, hd_center], - [0.5, separator_distance, separator_distance], - [aux_column_width, 0.5, separator_distance], - [separator_distance, 0.5, separator_distance], - [0.5, 0.5, separator_distance], - [z_max_r, z_max_r, 0.5], - [separator_distance, aux_column_width, 0.5], - [0.5, aux_column_width, 0.5], - [aux_column_width, separator_distance, 0.5], - [separator_distance, separator_distance, 0.5], - [0.5, separator_distance, 0.5], - [aux_column_width, 0.5, 0.5], - [separator_distance, 0.5, 0.5], - [0.5, 0.5, 0.5], - ] - ) + _np.array([0.5, 0.5, 0.5]) - - spline_list.append( - _Bezier(degrees=[2, 2, 2], control_points=x_max_y_max_z_max) - ) - return (spline_list, None) + return splines, derivatives From d425921219decc462b025f4eb075afe00c04b369 Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 9 Oct 2024 13:54:52 +0200 Subject: [PATCH 090/171] Fix derivative of Chi tile --- splinepy/microstructure/tiles/chi.py | 7 ++++--- tests/test_microstructure.py | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/splinepy/microstructure/tiles/chi.py b/splinepy/microstructure/tiles/chi.py index 827c7b8d3..0bcd215e9 100644 --- a/splinepy/microstructure/tiles/chi.py +++ b/splinepy/microstructure/tiles/chi.py @@ -83,12 +83,13 @@ def create_tile( s = r * _np.sin(alpha) c = r * _np.cos(alpha) else: - alpha = parameter_sensitivities[0, 0, i_derivative - 1] + alpha = parameters[0, 0] + _np.pi / 4 + dalpha = float(parameter_sensitivities[0, 0, i_derivative - 1]) v_one_half = 0.0 v_zero = 0.0 r = _np.sqrt(0.125) - s = r * _np.cos(alpha) - c = -r * _np.sin(alpha) + s = dalpha * r * _np.cos(alpha) + c = dalpha * -r * _np.sin(alpha) # Init return value spline_list = [] diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 73f8e1b72..edaf0b477 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -188,9 +188,8 @@ def test_tile_derivatives(np_rng, heps=1e-8, n_test_points=4): n_test_points: int Number of testing points in the parametric domain """ - # TODO: right now Chi, EllipsVoid, CubeVoid and InverseCross show wrong derivatives + # TODO: right now EllipsVoid, CubeVoid and InverseCross show wrong derivatives skip_classes = [ - ms.tiles.Chi, ms.tiles.EllipsVoid, ms.tiles.CubeVoid, ms.tiles.InverseCross3D, From c8be7ccb674af5858dded37ad174cf742bbbd23c Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 9 Oct 2024 15:47:47 +0200 Subject: [PATCH 091/171] Add/fix derivative of tile InverseCross3D --- .../microstructure/tiles/inverse_cross_3d.py | 1920 +++++++++-------- tests/test_microstructure.py | 8 +- 2 files changed, 1038 insertions(+), 890 deletions(-) diff --git a/splinepy/microstructure/tiles/inverse_cross_3d.py b/splinepy/microstructure/tiles/inverse_cross_3d.py index 4af1b2ef6..4787cd3b2 100644 --- a/splinepy/microstructure/tiles/inverse_cross_3d.py +++ b/splinepy/microstructure/tiles/inverse_cross_3d.py @@ -89,9 +89,13 @@ def _closing_tile( self.check_params(parameters) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + self.check_param_derivatives(parameter_sensitivities) + + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): raise ValueError("Thickness out of range (0, .5)") @@ -102,789 +106,933 @@ def _closing_tile( if not (0.0 < float(filling_height) < 1.0): raise ValueError("Filling must be in (0,1)") - # Precompute auxiliary values - inv_filling_height = 1.0 - filling_height - ctps_mid_height_top = (1 + filling_height) * 0.5 - ctps_mid_height_bottom = 1.0 - ctps_mid_height_top - center_width = 1.0 - 2 * boundary_width - r_center = center_width * 0.5 - half_r_center = (r_center + 0.5) * 0.5 - aux_column_width = 0.5 - 2 * (0.5 - separator_distance) - - spline_list = [] - if closure == "z_min": - branch_thickness = parameters.flatten()[5] - branch_neighbor_x_min_ctps = _np.array( - [ - [-0.5, -r_center, filling_height], - [-half_r_center, -r_center, filling_height], - [-r_center, -r_center, filling_height], - [-0.5, r_center, filling_height], - [-half_r_center, r_center, filling_height], - [-r_center, r_center, filling_height], - [-0.5, -aux_column_width, ctps_mid_height_top], - [ - -separator_distance, - -aux_column_width, - ctps_mid_height_top, - ], - [ - -branch_thickness, - -branch_thickness, - ctps_mid_height_top, - ], - [-0.5, aux_column_width, ctps_mid_height_top], - [ - -separator_distance, - aux_column_width, - ctps_mid_height_top, - ], - [-branch_thickness, branch_thickness, ctps_mid_height_top], - [-0.5, -aux_column_width, 1.0], - [-separator_distance, -aux_column_width, 1.0], - [-branch_thickness, -branch_thickness, 1.0], - [-0.5, aux_column_width, 1.0], - [-separator_distance, aux_column_width, 1.0], - [-branch_thickness, branch_thickness, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + splines = [] + + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + # Auxiliary values + fill_height_aux = filling_height + sep_distance_aux = separator_distance + inv_filling_height = 1.0 - filling_height + ctps_mid_height_top = (1 + filling_height) * 0.5 + ctps_mid_height_bottom = 1.0 - ctps_mid_height_top + center_width = 1.0 - 2 * boundary_width + r_center = center_width * 0.5 + half_r_center = (r_center + 0.5) * 0.5 + aux_column_width = 0.5 - 2 * (0.5 - separator_distance) + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + if closure == "z_min": + branch_thickness = parameters.flatten()[5] + elif closure == "z_max": + branch_thickness = parameters.flatten()[4] + else: + fill_height_aux = 0.0 + sep_distance_aux = 0.0 + inv_filling_height = 0.0 + ctps_mid_height_top = 0.0 + ctps_mid_height_bottom = 0.0 + center_width = 0.0 + r_center = 0.0 + half_r_center = 0.0 + aux_column_width = 0.0 + v_zero, v_one_half, v_one = [0.0] * 3 + if closure == "z_min": + branch_thickness = parameter_sensitivities.flatten()[5] + elif closure == "z_max": + branch_thickness = parameter_sensitivities.flatten()[4] + spline_list = [] + if closure == "z_min": + branch_neighbor_x_min_ctps = _np.array( + [ + [-v_one_half, -r_center, fill_height_aux], + [-half_r_center, -r_center, fill_height_aux], + [-r_center, -r_center, fill_height_aux], + [-v_one_half, r_center, fill_height_aux], + [-half_r_center, r_center, fill_height_aux], + [-r_center, r_center, fill_height_aux], + [-v_one_half, -aux_column_width, ctps_mid_height_top], + [ + -sep_distance_aux, + -aux_column_width, + ctps_mid_height_top, + ], + [ + -branch_thickness, + -branch_thickness, + ctps_mid_height_top, + ], + [-v_one_half, aux_column_width, ctps_mid_height_top], + [ + -sep_distance_aux, + aux_column_width, + ctps_mid_height_top, + ], + [ + -branch_thickness, + branch_thickness, + ctps_mid_height_top, + ], + [-v_one_half, -aux_column_width, v_one], + [-sep_distance_aux, -aux_column_width, v_one], + [-branch_thickness, -branch_thickness, v_one], + [-v_one_half, aux_column_width, v_one], + [-sep_distance_aux, aux_column_width, v_one], + [-branch_thickness, branch_thickness, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 1, 2], - control_points=branch_neighbor_x_min_ctps, + spline_list.append( + _Bezier( + degrees=[2, 1, 2], + control_points=branch_neighbor_x_min_ctps, + ) ) - ) - branch_neighbor_x_max_ctps = _np.array( - [ - [r_center, -r_center, filling_height], - [half_r_center, -r_center, filling_height], - [0.5, -r_center, filling_height], - [r_center, r_center, filling_height], - [half_r_center, r_center, filling_height], - [0.5, r_center, filling_height], - [branch_thickness, -branch_thickness, ctps_mid_height_top], - [ - separator_distance, - -aux_column_width, - ctps_mid_height_top, - ], - [0.5, -aux_column_width, ctps_mid_height_top], - [branch_thickness, branch_thickness, ctps_mid_height_top], + branch_neighbor_x_max_ctps = _np.array( [ - separator_distance, - aux_column_width, - ctps_mid_height_top, - ], - [0.5, aux_column_width, ctps_mid_height_top], - [branch_thickness, -branch_thickness, 1.0], - [separator_distance, -aux_column_width, 1.0], - [0.5, -aux_column_width, 1.0], - [branch_thickness, branch_thickness, 1.0], - [separator_distance, aux_column_width, 1.0], - [0.5, aux_column_width, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [r_center, -r_center, fill_height_aux], + [half_r_center, -r_center, fill_height_aux], + [v_one_half, -r_center, fill_height_aux], + [r_center, r_center, fill_height_aux], + [half_r_center, r_center, fill_height_aux], + [v_one_half, r_center, fill_height_aux], + [ + branch_thickness, + -branch_thickness, + ctps_mid_height_top, + ], + [ + sep_distance_aux, + -aux_column_width, + ctps_mid_height_top, + ], + [v_one_half, -aux_column_width, ctps_mid_height_top], + [ + branch_thickness, + branch_thickness, + ctps_mid_height_top, + ], + [ + sep_distance_aux, + aux_column_width, + ctps_mid_height_top, + ], + [v_one_half, aux_column_width, ctps_mid_height_top], + [branch_thickness, -branch_thickness, v_one], + [sep_distance_aux, -aux_column_width, v_one], + [v_one_half, -aux_column_width, v_one], + [branch_thickness, branch_thickness, v_one], + [sep_distance_aux, aux_column_width, v_one], + [v_one_half, aux_column_width, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 1, 2], - control_points=branch_neighbor_x_max_ctps, + spline_list.append( + _Bezier( + degrees=[2, 1, 2], + control_points=branch_neighbor_x_max_ctps, + ) ) - ) - branch_neighbor_y_min_ctps = _np.array( - [ - [-r_center, -0.5, filling_height], - [r_center, -0.5, filling_height], - [-r_center, -half_r_center, filling_height], - [r_center, -half_r_center, filling_height], - [-r_center, -r_center, filling_height], - [r_center, -r_center, filling_height], - [-aux_column_width, -0.5, ctps_mid_height_top], - [aux_column_width, -0.5, ctps_mid_height_top], - [ - -aux_column_width, - -separator_distance, - ctps_mid_height_top, - ], + branch_neighbor_y_min_ctps = _np.array( [ - aux_column_width, - -separator_distance, - ctps_mid_height_top, - ], - [ - -branch_thickness, - -branch_thickness, - ctps_mid_height_top, - ], - [branch_thickness, -branch_thickness, ctps_mid_height_top], - [-aux_column_width, -0.5, 1.0], - [aux_column_width, -0.5, 1.0], - [-aux_column_width, -separator_distance, 1.0], - [aux_column_width, -separator_distance, 1.0], - [-branch_thickness, -branch_thickness, 1.0], - [branch_thickness, -branch_thickness, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [-r_center, -v_one_half, fill_height_aux], + [r_center, -v_one_half, fill_height_aux], + [-r_center, -half_r_center, fill_height_aux], + [r_center, -half_r_center, fill_height_aux], + [-r_center, -r_center, fill_height_aux], + [r_center, -r_center, fill_height_aux], + [-aux_column_width, -v_one_half, ctps_mid_height_top], + [aux_column_width, -v_one_half, ctps_mid_height_top], + [ + -aux_column_width, + -sep_distance_aux, + ctps_mid_height_top, + ], + [ + aux_column_width, + -sep_distance_aux, + ctps_mid_height_top, + ], + [ + -branch_thickness, + -branch_thickness, + ctps_mid_height_top, + ], + [ + branch_thickness, + -branch_thickness, + ctps_mid_height_top, + ], + [-aux_column_width, -v_one_half, v_one], + [aux_column_width, -v_one_half, v_one], + [-aux_column_width, -sep_distance_aux, v_one], + [aux_column_width, -sep_distance_aux, v_one], + [-branch_thickness, -branch_thickness, v_one], + [branch_thickness, -branch_thickness, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[1, 2, 2], - control_points=branch_neighbor_y_min_ctps, + spline_list.append( + _Bezier( + degrees=[1, 2, 2], + control_points=branch_neighbor_y_min_ctps, + ) ) - ) - branch_neighbor_y_max_ctps = _np.array( - [ - [-r_center, r_center, filling_height], - [r_center, r_center, filling_height], - [-r_center, half_r_center, filling_height], - [r_center, half_r_center, filling_height], - [-r_center, 0.5, filling_height], - [r_center, 0.5, filling_height], - [-branch_thickness, branch_thickness, ctps_mid_height_top], - [branch_thickness, branch_thickness, ctps_mid_height_top], - [ - -aux_column_width, - separator_distance, - ctps_mid_height_top, - ], + branch_neighbor_y_max_ctps = _np.array( [ - aux_column_width, - separator_distance, - ctps_mid_height_top, - ], - [-aux_column_width, 0.5, ctps_mid_height_top], - [aux_column_width, 0.5, ctps_mid_height_top], - [-branch_thickness, branch_thickness, 1.0], - [branch_thickness, branch_thickness, 1.0], - [-aux_column_width, separator_distance, 1.0], - [aux_column_width, separator_distance, 1.0], - [-aux_column_width, 0.5, 1.0], - [aux_column_width, 0.5, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [-r_center, r_center, fill_height_aux], + [r_center, r_center, fill_height_aux], + [-r_center, half_r_center, fill_height_aux], + [r_center, half_r_center, fill_height_aux], + [-r_center, v_one_half, fill_height_aux], + [r_center, v_one_half, fill_height_aux], + [ + -branch_thickness, + branch_thickness, + ctps_mid_height_top, + ], + [ + branch_thickness, + branch_thickness, + ctps_mid_height_top, + ], + [ + -aux_column_width, + sep_distance_aux, + ctps_mid_height_top, + ], + [ + aux_column_width, + sep_distance_aux, + ctps_mid_height_top, + ], + [-aux_column_width, v_one_half, ctps_mid_height_top], + [aux_column_width, v_one_half, ctps_mid_height_top], + [-branch_thickness, branch_thickness, v_one], + [branch_thickness, branch_thickness, v_one], + [-aux_column_width, sep_distance_aux, v_one], + [aux_column_width, sep_distance_aux, v_one], + [-aux_column_width, v_one_half, v_one], + [aux_column_width, v_one_half, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[1, 2, 2], - control_points=branch_neighbor_y_max_ctps, + spline_list.append( + _Bezier( + degrees=[1, 2, 2], + control_points=branch_neighbor_y_max_ctps, + ) ) - ) - branch_x_min_y_min_ctps = _np.array( - [ - [-0.5, -0.5, filling_height], - [-half_r_center, -0.5, filling_height], - [-r_center, -0.5, filling_height], - [-0.5, -half_r_center, filling_height], - [-half_r_center, -half_r_center, filling_height], - [-r_center, -half_r_center, filling_height], - [-0.5, -r_center, filling_height], - [-half_r_center, -r_center, filling_height], - [-r_center, -r_center, filling_height], - [-0.5, -0.5, ctps_mid_height_top], - [-separator_distance, -0.5, ctps_mid_height_top], - [-aux_column_width, -0.5, ctps_mid_height_top], - [-0.5, -separator_distance, ctps_mid_height_top], - [ - -separator_distance, - -separator_distance, - ctps_mid_height_top, - ], - [ - -aux_column_width, - -separator_distance, - ctps_mid_height_top, - ], - [-0.5, -aux_column_width, ctps_mid_height_top], - [ - -separator_distance, - -aux_column_width, - ctps_mid_height_top, - ], - [ - -branch_thickness, - -branch_thickness, - ctps_mid_height_top, - ], - [-0.5, -0.5, 1.0], - [-separator_distance, -0.5, 1.0], - [-aux_column_width, -0.5, 1.0], - [-0.5, -separator_distance, 1.0], - [-separator_distance, -separator_distance, 1.0], - [-aux_column_width, -separator_distance, 1.0], - [-0.5, -aux_column_width, 1.0], - [-separator_distance, -aux_column_width, 1.0], - [-branch_thickness, -branch_thickness, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_min_y_min_ctps = _np.array( + [ + [-v_one_half, -v_one_half, fill_height_aux], + [-half_r_center, -v_one_half, fill_height_aux], + [-r_center, -v_one_half, fill_height_aux], + [-v_one_half, -half_r_center, fill_height_aux], + [-half_r_center, -half_r_center, fill_height_aux], + [-r_center, -half_r_center, fill_height_aux], + [-v_one_half, -r_center, fill_height_aux], + [-half_r_center, -r_center, fill_height_aux], + [-r_center, -r_center, fill_height_aux], + [-v_one_half, -v_one_half, ctps_mid_height_top], + [-sep_distance_aux, -v_one_half, ctps_mid_height_top], + [-aux_column_width, -v_one_half, ctps_mid_height_top], + [-v_one_half, -sep_distance_aux, ctps_mid_height_top], + [ + -sep_distance_aux, + -sep_distance_aux, + ctps_mid_height_top, + ], + [ + -aux_column_width, + -sep_distance_aux, + ctps_mid_height_top, + ], + [-v_one_half, -aux_column_width, ctps_mid_height_top], + [ + -sep_distance_aux, + -aux_column_width, + ctps_mid_height_top, + ], + [ + -branch_thickness, + -branch_thickness, + ctps_mid_height_top, + ], + [-v_one_half, -v_one_half, v_one], + [-sep_distance_aux, -v_one_half, v_one], + [-aux_column_width, -v_one_half, v_one], + [-v_one_half, -sep_distance_aux, v_one], + [-sep_distance_aux, -sep_distance_aux, v_one], + [-aux_column_width, -sep_distance_aux, v_one], + [-v_one_half, -aux_column_width, v_one], + [-sep_distance_aux, -aux_column_width, v_one], + [-branch_thickness, -branch_thickness, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_min_y_min_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_min_y_min_ctps, + ) ) - ) - branch_x_min_y_max_ctps = _np.array( - [ - [-0.5, r_center, filling_height], - [-half_r_center, r_center, filling_height], - [-r_center, r_center, filling_height], - [-0.5, half_r_center, filling_height], - [-half_r_center, half_r_center, filling_height], - [-r_center, half_r_center, filling_height], - [-0.5, 0.5, filling_height], - [-half_r_center, 0.5, filling_height], - [-r_center, 0.5, filling_height], - [-0.5, aux_column_width, ctps_mid_height_top], - [ - -separator_distance, - aux_column_width, - ctps_mid_height_top, - ], - [-branch_thickness, branch_thickness, ctps_mid_height_top], - [-0.5, separator_distance, ctps_mid_height_top], - [ - -separator_distance, - separator_distance, - ctps_mid_height_top, - ], - [ - -aux_column_width, - separator_distance, - ctps_mid_height_top, - ], - [-0.5, 0.5, ctps_mid_height_top], - [-separator_distance, 0.5, ctps_mid_height_top], - [-aux_column_width, 0.5, ctps_mid_height_top], - [-0.5, aux_column_width, 1.0], - [-separator_distance, aux_column_width, 1.0], - [-branch_thickness, branch_thickness, 1.0], - [-0.5, separator_distance, 1.0], - [-separator_distance, separator_distance, 1.0], - [-aux_column_width, separator_distance, 1.0], - [-0.5, 0.5, 1.0], - [-separator_distance, 0.5, 1.0], - [-aux_column_width, 0.5, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_min_y_max_ctps = _np.array( + [ + [-v_one_half, r_center, fill_height_aux], + [-half_r_center, r_center, fill_height_aux], + [-r_center, r_center, fill_height_aux], + [-v_one_half, half_r_center, fill_height_aux], + [-half_r_center, half_r_center, fill_height_aux], + [-r_center, half_r_center, fill_height_aux], + [-v_one_half, v_one_half, fill_height_aux], + [-half_r_center, v_one_half, fill_height_aux], + [-r_center, v_one_half, fill_height_aux], + [-v_one_half, aux_column_width, ctps_mid_height_top], + [ + -sep_distance_aux, + aux_column_width, + ctps_mid_height_top, + ], + [ + -branch_thickness, + branch_thickness, + ctps_mid_height_top, + ], + [-v_one_half, sep_distance_aux, ctps_mid_height_top], + [ + -sep_distance_aux, + sep_distance_aux, + ctps_mid_height_top, + ], + [ + -aux_column_width, + sep_distance_aux, + ctps_mid_height_top, + ], + [-v_one_half, v_one_half, ctps_mid_height_top], + [-sep_distance_aux, v_one_half, ctps_mid_height_top], + [-aux_column_width, v_one_half, ctps_mid_height_top], + [-v_one_half, aux_column_width, v_one], + [-sep_distance_aux, aux_column_width, v_one], + [-branch_thickness, branch_thickness, v_one], + [-v_one_half, sep_distance_aux, v_one], + [-sep_distance_aux, sep_distance_aux, v_one], + [-aux_column_width, sep_distance_aux, v_one], + [-v_one_half, v_one_half, v_one], + [-sep_distance_aux, v_one_half, v_one], + [-aux_column_width, v_one_half, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_min_y_max_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_min_y_max_ctps, + ) ) - ) - branch_x_max_y_min_ctps = _np.array( - [ - [r_center, -0.5, filling_height], - [half_r_center, -0.5, filling_height], - [0.5, -0.5, filling_height], - [r_center, -half_r_center, filling_height], - [half_r_center, -half_r_center, filling_height], - [0.5, -half_r_center, filling_height], - [r_center, -r_center, filling_height], - [half_r_center, -r_center, filling_height], - [0.5, -r_center, filling_height], - [aux_column_width, -0.5, ctps_mid_height_top], - [separator_distance, -0.5, ctps_mid_height_top], - [0.5, -0.5, ctps_mid_height_top], - [ - aux_column_width, - -separator_distance, - ctps_mid_height_top, - ], - [ - separator_distance, - -separator_distance, - ctps_mid_height_top, - ], - [0.5, -separator_distance, ctps_mid_height_top], - [branch_thickness, -branch_thickness, ctps_mid_height_top], - [ - separator_distance, - -aux_column_width, - ctps_mid_height_top, - ], - [0.5, -aux_column_width, ctps_mid_height_top], - [aux_column_width, -0.5, 1.0], - [separator_distance, -0.5, 1.0], - [0.5, -0.5, 1.0], - [aux_column_width, -separator_distance, 1.0], - [separator_distance, -separator_distance, 1.0], - [0.5, -separator_distance, 1.0], - [branch_thickness, -branch_thickness, 1.0], - [separator_distance, -aux_column_width, 1.0], - [0.5, -aux_column_width, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_max_y_min_ctps = _np.array( + [ + [r_center, -v_one_half, fill_height_aux], + [half_r_center, -v_one_half, fill_height_aux], + [v_one_half, -v_one_half, fill_height_aux], + [r_center, -half_r_center, fill_height_aux], + [half_r_center, -half_r_center, fill_height_aux], + [v_one_half, -half_r_center, fill_height_aux], + [r_center, -r_center, fill_height_aux], + [half_r_center, -r_center, fill_height_aux], + [v_one_half, -r_center, fill_height_aux], + [aux_column_width, -v_one_half, ctps_mid_height_top], + [sep_distance_aux, -v_one_half, ctps_mid_height_top], + [v_one_half, -v_one_half, ctps_mid_height_top], + [ + aux_column_width, + -sep_distance_aux, + ctps_mid_height_top, + ], + [ + sep_distance_aux, + -sep_distance_aux, + ctps_mid_height_top, + ], + [v_one_half, -sep_distance_aux, ctps_mid_height_top], + [ + branch_thickness, + -branch_thickness, + ctps_mid_height_top, + ], + [ + sep_distance_aux, + -aux_column_width, + ctps_mid_height_top, + ], + [v_one_half, -aux_column_width, ctps_mid_height_top], + [aux_column_width, -v_one_half, v_one], + [sep_distance_aux, -v_one_half, v_one], + [v_one_half, -v_one_half, v_one], + [aux_column_width, -sep_distance_aux, v_one], + [sep_distance_aux, -sep_distance_aux, v_one], + [v_one_half, -sep_distance_aux, v_one], + [branch_thickness, -branch_thickness, v_one], + [sep_distance_aux, -aux_column_width, v_one], + [v_one_half, -aux_column_width, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_max_y_min_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_max_y_min_ctps, + ) ) - ) - branch_x_max_y_max_ctps = _np.array( - [ - [r_center, r_center, filling_height], - [half_r_center, r_center, filling_height], - [0.5, r_center, filling_height], - [r_center, half_r_center, filling_height], - [half_r_center, half_r_center, filling_height], - [0.5, half_r_center, filling_height], - [r_center, 0.5, filling_height], - [half_r_center, 0.5, filling_height], - [0.5, 0.5, filling_height], - [branch_thickness, branch_thickness, ctps_mid_height_top], - [ - separator_distance, - aux_column_width, - ctps_mid_height_top, - ], - [0.5, aux_column_width, ctps_mid_height_top], - [ - aux_column_width, - separator_distance, - ctps_mid_height_top, - ], - [ - separator_distance, - separator_distance, - ctps_mid_height_top, - ], - [0.5, separator_distance, ctps_mid_height_top], - [aux_column_width, 0.5, ctps_mid_height_top], - [separator_distance, 0.5, ctps_mid_height_top], - [0.5, 0.5, ctps_mid_height_top], - [branch_thickness, branch_thickness, 1.0], - [separator_distance, aux_column_width, 1.0], - [0.5, aux_column_width, 1.0], - [aux_column_width, separator_distance, 1.0], - [separator_distance, separator_distance, 1.0], - [0.5, separator_distance, 1.0], - [aux_column_width, 0.5, 1.0], - [separator_distance, 0.5, 1.0], - [0.5, 0.5, 1.0], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_max_y_max_ctps = _np.array( + [ + [r_center, r_center, fill_height_aux], + [half_r_center, r_center, fill_height_aux], + [v_one_half, r_center, fill_height_aux], + [r_center, half_r_center, fill_height_aux], + [half_r_center, half_r_center, fill_height_aux], + [v_one_half, half_r_center, fill_height_aux], + [r_center, v_one_half, fill_height_aux], + [half_r_center, v_one_half, fill_height_aux], + [v_one_half, v_one_half, fill_height_aux], + [ + branch_thickness, + branch_thickness, + ctps_mid_height_top, + ], + [ + sep_distance_aux, + aux_column_width, + ctps_mid_height_top, + ], + [v_one_half, aux_column_width, ctps_mid_height_top], + [ + aux_column_width, + sep_distance_aux, + ctps_mid_height_top, + ], + [ + sep_distance_aux, + sep_distance_aux, + ctps_mid_height_top, + ], + [v_one_half, sep_distance_aux, ctps_mid_height_top], + [aux_column_width, v_one_half, ctps_mid_height_top], + [sep_distance_aux, v_one_half, ctps_mid_height_top], + [v_one_half, v_one_half, ctps_mid_height_top], + [branch_thickness, branch_thickness, v_one], + [sep_distance_aux, aux_column_width, v_one], + [v_one_half, aux_column_width, v_one], + [aux_column_width, sep_distance_aux, v_one], + [sep_distance_aux, sep_distance_aux, v_one], + [v_one_half, sep_distance_aux, v_one], + [aux_column_width, v_one_half, v_one], + [sep_distance_aux, v_one_half, v_one], + [v_one_half, v_one_half, v_one], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_max_y_max_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_max_y_max_ctps, + ) ) - ) - - return (spline_list, None) - elif closure == "z_max": - branch_thickness = parameters.flatten()[4] - branch_neighbor_x_min_ctps = _np.array( - [ - [-0.5, -aux_column_width, 0.0], - [-separator_distance, -aux_column_width, 0.0], - [-branch_thickness, -branch_thickness, 0.0], - [-0.5, aux_column_width, 0.0], - [-separator_distance, aux_column_width, 0.0], - [-branch_thickness, branch_thickness, 0.0], - [-0.5, -aux_column_width, ctps_mid_height_bottom], - [ - -separator_distance, - -aux_column_width, - ctps_mid_height_bottom, - ], - [ - -branch_thickness, - -branch_thickness, - ctps_mid_height_bottom, - ], - [-0.5, aux_column_width, ctps_mid_height_bottom], + elif closure == "z_max": + branch_neighbor_x_min_ctps = _np.array( [ - -separator_distance, - aux_column_width, - ctps_mid_height_bottom, - ], - [ - -branch_thickness, - branch_thickness, - ctps_mid_height_bottom, - ], - [-0.5, -r_center, inv_filling_height], - [-half_r_center, -r_center, inv_filling_height], - [-r_center, -r_center, inv_filling_height], - [-0.5, r_center, inv_filling_height], - [-half_r_center, r_center, inv_filling_height], - [-r_center, r_center, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [-v_one_half, -aux_column_width, v_zero], + [-sep_distance_aux, -aux_column_width, v_zero], + [-branch_thickness, -branch_thickness, v_zero], + [-v_one_half, aux_column_width, v_zero], + [-sep_distance_aux, aux_column_width, v_zero], + [-branch_thickness, branch_thickness, v_zero], + [ + -v_one_half, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + -sep_distance_aux, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + -branch_thickness, + -branch_thickness, + ctps_mid_height_bottom, + ], + [ + -v_one_half, + aux_column_width, + ctps_mid_height_bottom, + ], + [ + -sep_distance_aux, + aux_column_width, + ctps_mid_height_bottom, + ], + [ + -branch_thickness, + branch_thickness, + ctps_mid_height_bottom, + ], + [-v_one_half, -r_center, inv_filling_height], + [-half_r_center, -r_center, inv_filling_height], + [-r_center, -r_center, inv_filling_height], + [-v_one_half, r_center, inv_filling_height], + [-half_r_center, r_center, inv_filling_height], + [-r_center, r_center, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 1, 2], - control_points=branch_neighbor_x_min_ctps, + spline_list.append( + _Bezier( + degrees=[2, 1, 2], + control_points=branch_neighbor_x_min_ctps, + ) ) - ) - branch_neighbor_x_max_ctps = _np.array( - [ - [branch_thickness, -branch_thickness, 0.0], - [separator_distance, -aux_column_width, 0.0], - [0.5, -aux_column_width, 0.0], - [branch_thickness, branch_thickness, 0.0], - [separator_distance, aux_column_width, 0.0], - [0.5, aux_column_width, 0.0], - [ - branch_thickness, - -branch_thickness, - ctps_mid_height_bottom, - ], - [ - separator_distance, - -aux_column_width, - ctps_mid_height_bottom, - ], - [0.5, -aux_column_width, ctps_mid_height_bottom], - [ - branch_thickness, - branch_thickness, - ctps_mid_height_bottom, - ], + branch_neighbor_x_max_ctps = _np.array( [ - separator_distance, - aux_column_width, - ctps_mid_height_bottom, - ], - [0.5, aux_column_width, ctps_mid_height_bottom], - [r_center, -r_center, inv_filling_height], - [half_r_center, -r_center, inv_filling_height], - [0.5, -r_center, inv_filling_height], - [r_center, r_center, inv_filling_height], - [half_r_center, r_center, inv_filling_height], - [0.5, r_center, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [branch_thickness, -branch_thickness, v_zero], + [sep_distance_aux, -aux_column_width, v_zero], + [v_one_half, -aux_column_width, v_zero], + [branch_thickness, branch_thickness, v_zero], + [sep_distance_aux, aux_column_width, v_zero], + [v_one_half, aux_column_width, v_zero], + [ + branch_thickness, + -branch_thickness, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + v_one_half, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + branch_thickness, + branch_thickness, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + aux_column_width, + ctps_mid_height_bottom, + ], + [v_one_half, aux_column_width, ctps_mid_height_bottom], + [r_center, -r_center, inv_filling_height], + [half_r_center, -r_center, inv_filling_height], + [v_one_half, -r_center, inv_filling_height], + [r_center, r_center, inv_filling_height], + [half_r_center, r_center, inv_filling_height], + [v_one_half, r_center, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 1, 2], - control_points=branch_neighbor_x_max_ctps, + spline_list.append( + _Bezier( + degrees=[2, 1, 2], + control_points=branch_neighbor_x_max_ctps, + ) ) - ) - branch_neighbor_y_min_ctps = _np.array( - [ - [-aux_column_width, -0.5, 0.0], - [aux_column_width, -0.5, 0.0], - [-aux_column_width, -separator_distance, 0.0], - [aux_column_width, -separator_distance, 0.0], - [-branch_thickness, -branch_thickness, 0.0], - [branch_thickness, -branch_thickness, 0.0], - [-aux_column_width, -0.5, ctps_mid_height_bottom], - [aux_column_width, -0.5, ctps_mid_height_bottom], - [ - -aux_column_width, - -separator_distance, - ctps_mid_height_bottom, - ], + branch_neighbor_y_min_ctps = _np.array( [ - aux_column_width, - -separator_distance, - ctps_mid_height_bottom, - ], - [ - -branch_thickness, - -branch_thickness, - ctps_mid_height_bottom, - ], - [ - branch_thickness, - -branch_thickness, - ctps_mid_height_bottom, - ], - [-r_center, -0.5, inv_filling_height], - [r_center, -0.5, inv_filling_height], - [-r_center, -half_r_center, inv_filling_height], - [r_center, -half_r_center, inv_filling_height], - [-r_center, -r_center, inv_filling_height], - [r_center, -r_center, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [-aux_column_width, -v_one_half, v_zero], + [aux_column_width, -v_one_half, v_zero], + [-aux_column_width, -sep_distance_aux, v_zero], + [aux_column_width, -sep_distance_aux, v_zero], + [-branch_thickness, -branch_thickness, v_zero], + [branch_thickness, -branch_thickness, v_zero], + [ + -aux_column_width, + -v_one_half, + ctps_mid_height_bottom, + ], + [ + aux_column_width, + -v_one_half, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + aux_column_width, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -branch_thickness, + -branch_thickness, + ctps_mid_height_bottom, + ], + [ + branch_thickness, + -branch_thickness, + ctps_mid_height_bottom, + ], + [-r_center, -v_one_half, inv_filling_height], + [r_center, -v_one_half, inv_filling_height], + [-r_center, -half_r_center, inv_filling_height], + [r_center, -half_r_center, inv_filling_height], + [-r_center, -r_center, inv_filling_height], + [r_center, -r_center, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[1, 2, 2], - control_points=branch_neighbor_y_min_ctps, + spline_list.append( + _Bezier( + degrees=[1, 2, 2], + control_points=branch_neighbor_y_min_ctps, + ) ) - ) - branch_neighbor_y_max_ctps = _np.array( - [ - [-branch_thickness, branch_thickness, 0.0], - [branch_thickness, branch_thickness, 0.0], - [-aux_column_width, separator_distance, 0.0], - [aux_column_width, separator_distance, 0.0], - [-aux_column_width, 0.5, 0.0], - [aux_column_width, 0.5, 0.0], + branch_neighbor_y_max_ctps = _np.array( [ - -branch_thickness, - branch_thickness, - ctps_mid_height_bottom, - ], - [ - branch_thickness, - branch_thickness, - ctps_mid_height_bottom, - ], - [ - -aux_column_width, - separator_distance, - ctps_mid_height_bottom, - ], - [ - aux_column_width, - separator_distance, - ctps_mid_height_bottom, - ], - [-aux_column_width, 0.5, ctps_mid_height_bottom], - [aux_column_width, 0.5, ctps_mid_height_bottom], - [-r_center, r_center, inv_filling_height], - [r_center, r_center, inv_filling_height], - [-r_center, half_r_center, inv_filling_height], - [r_center, half_r_center, inv_filling_height], - [-r_center, 0.5, inv_filling_height], - [r_center, 0.5, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + [-branch_thickness, branch_thickness, v_zero], + [branch_thickness, branch_thickness, v_zero], + [-aux_column_width, sep_distance_aux, v_zero], + [aux_column_width, sep_distance_aux, v_zero], + [-aux_column_width, v_one_half, v_zero], + [aux_column_width, v_one_half, v_zero], + [ + -branch_thickness, + branch_thickness, + ctps_mid_height_bottom, + ], + [ + branch_thickness, + branch_thickness, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + aux_column_width, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + v_one_half, + ctps_mid_height_bottom, + ], + [aux_column_width, v_one_half, ctps_mid_height_bottom], + [-r_center, r_center, inv_filling_height], + [r_center, r_center, inv_filling_height], + [-r_center, half_r_center, inv_filling_height], + [r_center, half_r_center, inv_filling_height], + [-r_center, v_one_half, inv_filling_height], + [r_center, v_one_half, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[1, 2, 2], - control_points=branch_neighbor_y_max_ctps, + spline_list.append( + _Bezier( + degrees=[1, 2, 2], + control_points=branch_neighbor_y_max_ctps, + ) ) - ) - branch_x_min_y_min_ctps = _np.array( - [ - [-0.5, -0.5, 0.0], - [-separator_distance, -0.5, 0.0], - [-aux_column_width, -0.5, 0.0], - [-0.5, -separator_distance, 0.0], - [-separator_distance, -separator_distance, 0.0], - [-aux_column_width, -separator_distance, 0.0], - [-0.5, -aux_column_width, 0.0], - [-separator_distance, -aux_column_width, 0.0], - [-branch_thickness, -branch_thickness, 0.0], - [-0.5, -0.5, ctps_mid_height_bottom], - [-separator_distance, -0.5, ctps_mid_height_bottom], - [-aux_column_width, -0.5, ctps_mid_height_bottom], - [-0.5, -separator_distance, ctps_mid_height_bottom], - [ - -separator_distance, - -separator_distance, - ctps_mid_height_bottom, - ], - [ - -aux_column_width, - -separator_distance, - ctps_mid_height_bottom, - ], - [-0.5, -aux_column_width, ctps_mid_height_bottom], - [ - -separator_distance, - -aux_column_width, - ctps_mid_height_bottom, - ], - [ - -branch_thickness, - -branch_thickness, - ctps_mid_height_bottom, - ], - [-0.5, -0.5, inv_filling_height], - [-half_r_center, -0.5, inv_filling_height], - [-r_center, -0.5, inv_filling_height], - [-0.5, -half_r_center, inv_filling_height], - [-half_r_center, -half_r_center, inv_filling_height], - [-r_center, -half_r_center, inv_filling_height], - [-0.5, -r_center, inv_filling_height], - [-half_r_center, -r_center, inv_filling_height], - [-r_center, -r_center, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_min_y_min_ctps = _np.array( + [ + [-v_one_half, -v_one_half, v_zero], + [-sep_distance_aux, -v_one_half, v_zero], + [-aux_column_width, -v_one_half, v_zero], + [-v_one_half, -sep_distance_aux, v_zero], + [-sep_distance_aux, -sep_distance_aux, v_zero], + [-aux_column_width, -sep_distance_aux, v_zero], + [-v_one_half, -aux_column_width, v_zero], + [-sep_distance_aux, -aux_column_width, v_zero], + [-branch_thickness, -branch_thickness, v_zero], + [-v_one_half, -v_one_half, ctps_mid_height_bottom], + [ + -sep_distance_aux, + -v_one_half, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + -v_one_half, + ctps_mid_height_bottom, + ], + [ + -v_one_half, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -sep_distance_aux, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -v_one_half, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + -sep_distance_aux, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + -branch_thickness, + -branch_thickness, + ctps_mid_height_bottom, + ], + [-v_one_half, -v_one_half, inv_filling_height], + [-half_r_center, -v_one_half, inv_filling_height], + [-r_center, -v_one_half, inv_filling_height], + [-v_one_half, -half_r_center, inv_filling_height], + [-half_r_center, -half_r_center, inv_filling_height], + [-r_center, -half_r_center, inv_filling_height], + [-v_one_half, -r_center, inv_filling_height], + [-half_r_center, -r_center, inv_filling_height], + [-r_center, -r_center, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_min_y_min_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_min_y_min_ctps, + ) ) - ) - branch_x_max_y_max_ctps = _np.array( - [ - [branch_thickness, branch_thickness, 0.0], - [separator_distance, aux_column_width, 0.0], - [0.5, aux_column_width, 0.0], - [aux_column_width, separator_distance, 0.0], - [separator_distance, separator_distance, 0.0], - [0.5, separator_distance, 0.0], - [aux_column_width, 0.5, 0.0], - [separator_distance, 0.5, 0.0], - [0.5, 0.5, 0.0], - [ - branch_thickness, - branch_thickness, - ctps_mid_height_bottom, - ], - [ - separator_distance, - aux_column_width, - ctps_mid_height_bottom, - ], - [0.5, aux_column_width, ctps_mid_height_bottom], - [ - aux_column_width, - separator_distance, - ctps_mid_height_bottom, - ], - [ - separator_distance, - separator_distance, - ctps_mid_height_bottom, - ], - [0.5, separator_distance, ctps_mid_height_bottom], - [aux_column_width, 0.5, ctps_mid_height_bottom], - [separator_distance, 0.5, ctps_mid_height_bottom], - [0.5, 0.5, ctps_mid_height_bottom], - [r_center, r_center, inv_filling_height], - [half_r_center, r_center, inv_filling_height], - [0.5, r_center, inv_filling_height], - [r_center, half_r_center, inv_filling_height], - [half_r_center, half_r_center, inv_filling_height], - [0.5, half_r_center, inv_filling_height], - [r_center, 0.5, inv_filling_height], - [half_r_center, 0.5, inv_filling_height], - [0.5, 0.5, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_max_y_max_ctps = _np.array( + [ + [branch_thickness, branch_thickness, v_zero], + [sep_distance_aux, aux_column_width, v_zero], + [v_one_half, aux_column_width, v_zero], + [aux_column_width, sep_distance_aux, v_zero], + [sep_distance_aux, sep_distance_aux, v_zero], + [v_one_half, sep_distance_aux, v_zero], + [aux_column_width, v_one_half, v_zero], + [sep_distance_aux, v_one_half, v_zero], + [v_one_half, v_one_half, v_zero], + [ + branch_thickness, + branch_thickness, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + aux_column_width, + ctps_mid_height_bottom, + ], + [v_one_half, aux_column_width, ctps_mid_height_bottom], + [ + aux_column_width, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [v_one_half, sep_distance_aux, ctps_mid_height_bottom], + [aux_column_width, v_one_half, ctps_mid_height_bottom], + [sep_distance_aux, v_one_half, ctps_mid_height_bottom], + [v_one_half, v_one_half, ctps_mid_height_bottom], + [r_center, r_center, inv_filling_height], + [half_r_center, r_center, inv_filling_height], + [v_one_half, r_center, inv_filling_height], + [r_center, half_r_center, inv_filling_height], + [half_r_center, half_r_center, inv_filling_height], + [v_one_half, half_r_center, inv_filling_height], + [r_center, v_one_half, inv_filling_height], + [half_r_center, v_one_half, inv_filling_height], + [v_one_half, v_one_half, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_max_y_max_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_max_y_max_ctps, + ) ) - ) - branch_x_max_y_min_ctps = _np.array( - [ - [aux_column_width, -0.5, 0.0], - [separator_distance, -0.5, 0.0], - [0.5, -0.5, 0.0], - [aux_column_width, -separator_distance, 0.0], - [separator_distance, -separator_distance, 0.0], - [0.5, -separator_distance, 0.0], - [branch_thickness, -branch_thickness, 0.0], - [separator_distance, -aux_column_width, 0.0], - [0.5, -aux_column_width, 0.0], - [aux_column_width, -0.5, ctps_mid_height_bottom], - [separator_distance, -0.5, ctps_mid_height_bottom], - [0.5, -0.5, ctps_mid_height_bottom], - [ - aux_column_width, - -separator_distance, - ctps_mid_height_bottom, - ], - [ - separator_distance, - -separator_distance, - ctps_mid_height_bottom, - ], - [0.5, -separator_distance, ctps_mid_height_bottom], - [ - branch_thickness, - -branch_thickness, - ctps_mid_height_bottom, - ], - [ - separator_distance, - -aux_column_width, - ctps_mid_height_bottom, - ], - [0.5, -aux_column_width, ctps_mid_height_bottom], - [r_center, -0.5, inv_filling_height], - [half_r_center, -0.5, inv_filling_height], - [0.5, -0.5, inv_filling_height], - [r_center, -half_r_center, inv_filling_height], - [half_r_center, -half_r_center, inv_filling_height], - [0.5, -half_r_center, inv_filling_height], - [r_center, -r_center, inv_filling_height], - [half_r_center, -r_center, inv_filling_height], - [0.5, -r_center, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_max_y_min_ctps = _np.array( + [ + [aux_column_width, -v_one_half, v_zero], + [sep_distance_aux, -v_one_half, v_zero], + [v_one_half, -v_one_half, v_zero], + [aux_column_width, -sep_distance_aux, v_zero], + [sep_distance_aux, -sep_distance_aux, v_zero], + [v_one_half, -sep_distance_aux, v_zero], + [branch_thickness, -branch_thickness, v_zero], + [sep_distance_aux, -aux_column_width, v_zero], + [v_one_half, -aux_column_width, v_zero], + [ + aux_column_width, + -v_one_half, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + -v_one_half, + ctps_mid_height_bottom, + ], + [v_one_half, -v_one_half, ctps_mid_height_bottom], + [ + aux_column_width, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + v_one_half, + -sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + branch_thickness, + -branch_thickness, + ctps_mid_height_bottom, + ], + [ + sep_distance_aux, + -aux_column_width, + ctps_mid_height_bottom, + ], + [ + v_one_half, + -aux_column_width, + ctps_mid_height_bottom, + ], + [r_center, -v_one_half, inv_filling_height], + [half_r_center, -v_one_half, inv_filling_height], + [v_one_half, -v_one_half, inv_filling_height], + [r_center, -half_r_center, inv_filling_height], + [half_r_center, -half_r_center, inv_filling_height], + [v_one_half, -half_r_center, inv_filling_height], + [r_center, -r_center, inv_filling_height], + [half_r_center, -r_center, inv_filling_height], + [v_one_half, -r_center, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_max_y_min_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_max_y_min_ctps, + ) ) - ) - branch_x_min_y_max_ctps = _np.array( - [ - [-0.5, aux_column_width, 0.0], - [-separator_distance, aux_column_width, 0.0], - [-branch_thickness, branch_thickness, 0.0], - [-0.5, separator_distance, 0.0], - [-separator_distance, separator_distance, 0.0], - [-aux_column_width, separator_distance, 0.0], - [-0.5, 0.5, 0.0], - [-separator_distance, 0.5, 0.0], - [-aux_column_width, 0.5, 0.0], - [-0.5, aux_column_width, ctps_mid_height_bottom], - [ - -separator_distance, - aux_column_width, - ctps_mid_height_bottom, - ], - [ - -branch_thickness, - branch_thickness, - ctps_mid_height_bottom, - ], - [-0.5, separator_distance, ctps_mid_height_bottom], - [ - -separator_distance, - separator_distance, - ctps_mid_height_bottom, - ], - [ - -aux_column_width, - separator_distance, - ctps_mid_height_bottom, - ], - [-0.5, 0.5, ctps_mid_height_bottom], - [-separator_distance, 0.5, ctps_mid_height_bottom], - [-aux_column_width, 0.5, ctps_mid_height_bottom], - [-0.5, r_center, inv_filling_height], - [-half_r_center, r_center, inv_filling_height], - [-r_center, r_center, inv_filling_height], - [-0.5, half_r_center, inv_filling_height], - [-half_r_center, half_r_center, inv_filling_height], - [-r_center, half_r_center, inv_filling_height], - [-0.5, 0.5, inv_filling_height], - [-half_r_center, 0.5, inv_filling_height], - [-r_center, 0.5, inv_filling_height], - ] - ) + _np.array([0.5, 0.5, 0.0]) + branch_x_min_y_max_ctps = _np.array( + [ + [-v_one_half, aux_column_width, v_zero], + [-sep_distance_aux, aux_column_width, v_zero], + [-branch_thickness, branch_thickness, v_zero], + [-v_one_half, sep_distance_aux, v_zero], + [-sep_distance_aux, sep_distance_aux, v_zero], + [-aux_column_width, sep_distance_aux, v_zero], + [-v_one_half, v_one_half, v_zero], + [-sep_distance_aux, v_one_half, v_zero], + [-aux_column_width, v_one_half, v_zero], + [ + -v_one_half, + aux_column_width, + ctps_mid_height_bottom, + ], + [ + -sep_distance_aux, + aux_column_width, + ctps_mid_height_bottom, + ], + [ + -branch_thickness, + branch_thickness, + ctps_mid_height_bottom, + ], + [ + -v_one_half, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -sep_distance_aux, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + sep_distance_aux, + ctps_mid_height_bottom, + ], + [-v_one_half, v_one_half, ctps_mid_height_bottom], + [ + -sep_distance_aux, + v_one_half, + ctps_mid_height_bottom, + ], + [ + -aux_column_width, + v_one_half, + ctps_mid_height_bottom, + ], + [-v_one_half, r_center, inv_filling_height], + [-half_r_center, r_center, inv_filling_height], + [-r_center, r_center, inv_filling_height], + [-v_one_half, half_r_center, inv_filling_height], + [-half_r_center, half_r_center, inv_filling_height], + [-r_center, half_r_center, inv_filling_height], + [-v_one_half, v_one_half, inv_filling_height], + [-half_r_center, v_one_half, inv_filling_height], + [-r_center, v_one_half, inv_filling_height], + ] + ) + _np.array([v_one_half, v_one_half, v_zero]) - spline_list.append( - _Bezier( - degrees=[2, 2, 2], control_points=branch_x_min_y_max_ctps + spline_list.append( + _Bezier( + degrees=[2, 2, 2], + control_points=branch_x_min_y_max_ctps, + ) ) - ) + else: + raise ValueError("Corner Type not supported") - return (spline_list, None) - else: - raise ValueError("Corner Type not supported") + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) + + return (splines, derivatives) def create_tile( self, @@ -922,7 +1070,7 @@ def create_tile( """ if not isinstance(center_expansion, float): - raise ValueError("Invalid Type") + raise ValueError("Invalid type for center_expansion") if not ((center_expansion > 0.5) and (center_expansion < 1.5)): raise ValueError("Center Expansion must be in (.5, 1.5)") @@ -942,16 +1090,17 @@ def create_tile( * 0.2 ) + self.check_params(parameters) + if parameter_sensitivities is not None: + self.check_param_derivatives(parameter_sensitivities) + n_derivatives = parameter_sensitivities.shape[2] derivatives = [] else: n_derivatives = 0 derivatives = None - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - if _np.any(parameters < min_radius) or _np.any( parameters > max_radius ): @@ -999,6 +1148,8 @@ def create_tile( v_one_half = 0.5 center_point = _np.array([0.5, 0.5, 0.5]) + + sep_distance = separator_distance else: sensitivities_i = parameter_sensitivities[ :, 0, i_derivative - 1 @@ -1032,6 +1183,8 @@ def create_tile( v_one_half = 0.0 center_point = _np.zeros(3) + sep_distance = 0.0 + # Init return type spline_list = [] @@ -1040,18 +1193,18 @@ def create_tile( _np.array( [ [-v_one_half, -v_one_half, -aux_column_width], - [-separator_distance, -v_one_half, -aux_column_width], + [-sep_distance, -v_one_half, -aux_column_width], [-y_min_r, -v_one_half, -y_min_r], - [-v_one_half, -separator_distance, -aux_column_width], + [-v_one_half, -sep_distance, -aux_column_width], [-hd_center, -hd_center, -aux_column_width], [-aux_y_min, -hd_center, -aux_y_min], [-v_one_half, -x_min_r, -x_min_r], [-hd_center, -aux_x_min, -aux_x_min], [-center_r, -center_r, -center_r], [-v_one_half, -v_one_half, aux_column_width], - [-separator_distance, -v_one_half, aux_column_width], + [-sep_distance, -v_one_half, aux_column_width], [-y_min_r, -v_one_half, y_min_r], - [-v_one_half, -separator_distance, aux_column_width], + [-v_one_half, -sep_distance, aux_column_width], [-hd_center, -hd_center, aux_column_width], [-aux_y_min, -hd_center, aux_y_min], [-v_one_half, -x_min_r, x_min_r], @@ -1066,20 +1219,20 @@ def create_tile( _np.array( [ [y_min_r, -v_one_half, -y_min_r], - [separator_distance, -v_one_half, -aux_column_width], + [sep_distance, -v_one_half, -aux_column_width], [v_one_half, -v_one_half, -aux_column_width], [aux_y_min, -hd_center, -aux_y_min], [hd_center, -hd_center, -aux_column_width], - [v_one_half, -separator_distance, -aux_column_width], + [v_one_half, -sep_distance, -aux_column_width], [center_r, -center_r, -center_r], [hd_center, -aux_x_max, -aux_x_max], [v_one_half, -x_max_r, -x_max_r], [y_min_r, -v_one_half, y_min_r], - [separator_distance, -v_one_half, aux_column_width], + [sep_distance, -v_one_half, aux_column_width], [v_one_half, -v_one_half, aux_column_width], [aux_y_min, -hd_center, aux_y_min], [hd_center, -hd_center, aux_column_width], - [v_one_half, -separator_distance, aux_column_width], + [v_one_half, -sep_distance, aux_column_width], [center_r, -center_r, center_r], [hd_center, -aux_x_max, aux_x_max], [v_one_half, -x_max_r, x_max_r], @@ -1094,20 +1247,20 @@ def create_tile( [-v_one_half, x_min_r, -x_min_r], [-hd_center, aux_x_min, -aux_x_min], [-center_r, center_r, -center_r], - [-v_one_half, separator_distance, -aux_column_width], + [-v_one_half, sep_distance, -aux_column_width], [-hd_center, hd_center, -aux_column_width], [-aux_y_max, hd_center, -aux_y_max], [-v_one_half, v_one_half, -aux_column_width], - [-separator_distance, v_one_half, -aux_column_width], + [-sep_distance, v_one_half, -aux_column_width], [-y_max_r, v_one_half, -y_max_r], [-v_one_half, x_min_r, x_min_r], [-hd_center, aux_x_min, aux_x_min], [-center_r, center_r, center_r], - [-v_one_half, separator_distance, aux_column_width], + [-v_one_half, sep_distance, aux_column_width], [-hd_center, hd_center, aux_column_width], [-aux_y_max, hd_center, aux_y_max], [-v_one_half, v_one_half, aux_column_width], - [-separator_distance, v_one_half, aux_column_width], + [-sep_distance, v_one_half, aux_column_width], [-y_max_r, v_one_half, y_max_r], ] ) @@ -1122,18 +1275,18 @@ def create_tile( [v_one_half, x_max_r, -x_max_r], [aux_y_max, hd_center, -aux_y_max], [hd_center, hd_center, -aux_column_width], - [v_one_half, separator_distance, -aux_column_width], + [v_one_half, sep_distance, -aux_column_width], [y_max_r, v_one_half, -y_max_r], - [separator_distance, v_one_half, -aux_column_width], + [sep_distance, v_one_half, -aux_column_width], [v_one_half, v_one_half, -aux_column_width], [center_r, center_r, center_r], [hd_center, aux_x_max, aux_x_max], [v_one_half, x_max_r, x_max_r], [aux_y_max, hd_center, aux_y_max], [hd_center, hd_center, aux_column_width], - [v_one_half, separator_distance, aux_column_width], + [v_one_half, sep_distance, aux_column_width], [y_max_r, v_one_half, y_max_r], - [separator_distance, v_one_half, aux_column_width], + [sep_distance, v_one_half, aux_column_width], [v_one_half, v_one_half, aux_column_width], ] ) @@ -1144,15 +1297,15 @@ def create_tile( _np.array( [ [-v_one_half, -aux_column_width, -v_one_half], - [-separator_distance, -aux_column_width, -v_one_half], + [-sep_distance, -aux_column_width, -v_one_half], [-z_min_r, -z_min_r, -v_one_half], [-v_one_half, aux_column_width, -v_one_half], - [-separator_distance, aux_column_width, -v_one_half], + [-sep_distance, aux_column_width, -v_one_half], [-z_min_r, z_min_r, -v_one_half], - [-v_one_half, -aux_column_width, -separator_distance], + [-v_one_half, -aux_column_width, -sep_distance], [-hd_center, -aux_column_width, -hd_center], [-aux_z_min, -aux_z_min, -hd_center], - [-v_one_half, aux_column_width, -separator_distance], + [-v_one_half, aux_column_width, -sep_distance], [-hd_center, aux_column_width, -hd_center], [-aux_z_min, aux_z_min, -hd_center], [-v_one_half, -x_min_r, -x_min_r], @@ -1170,17 +1323,17 @@ def create_tile( _np.array( [ [z_min_r, -z_min_r, -v_one_half], - [separator_distance, -aux_column_width, -v_one_half], + [sep_distance, -aux_column_width, -v_one_half], [v_one_half, -aux_column_width, -v_one_half], [z_min_r, z_min_r, -v_one_half], - [separator_distance, aux_column_width, -v_one_half], + [sep_distance, aux_column_width, -v_one_half], [v_one_half, aux_column_width, -v_one_half], [aux_z_min, -aux_z_min, -hd_center], [hd_center, -aux_column_width, -hd_center], - [v_one_half, -aux_column_width, -separator_distance], + [v_one_half, -aux_column_width, -sep_distance], [aux_z_min, aux_z_min, -hd_center], [hd_center, aux_column_width, -hd_center], - [v_one_half, aux_column_width, -separator_distance], + [v_one_half, aux_column_width, -sep_distance], [center_r, -center_r, -center_r], [hd_center, -aux_x_max, -aux_x_max], [v_one_half, -x_max_r, -x_max_r], @@ -1201,17 +1354,17 @@ def create_tile( [-v_one_half, x_min_r, x_min_r], [-hd_center, aux_x_min, aux_x_min], [-center_r, center_r, center_r], - [-v_one_half, -aux_column_width, separator_distance], + [-v_one_half, -aux_column_width, sep_distance], [-hd_center, -aux_column_width, hd_center], [-aux_z_max, -aux_z_max, hd_center], - [-v_one_half, aux_column_width, separator_distance], + [-v_one_half, aux_column_width, sep_distance], [-hd_center, aux_column_width, hd_center], [-aux_z_max, aux_z_max, hd_center], [-v_one_half, -aux_column_width, v_one_half], - [-separator_distance, -aux_column_width, v_one_half], + [-sep_distance, -aux_column_width, v_one_half], [-z_max_r, -z_max_r, v_one_half], [-v_one_half, aux_column_width, v_one_half], - [-separator_distance, aux_column_width, v_one_half], + [-sep_distance, aux_column_width, v_one_half], [-z_max_r, z_max_r, v_one_half], ] ) @@ -1229,15 +1382,15 @@ def create_tile( [v_one_half, x_max_r, x_max_r], [aux_z_max, -aux_z_max, hd_center], [hd_center, -aux_column_width, hd_center], - [v_one_half, -aux_column_width, separator_distance], + [v_one_half, -aux_column_width, sep_distance], [aux_z_max, aux_z_max, hd_center], [hd_center, aux_column_width, hd_center], - [v_one_half, aux_column_width, separator_distance], + [v_one_half, aux_column_width, sep_distance], [z_max_r, -z_max_r, v_one_half], - [separator_distance, -aux_column_width, v_one_half], + [sep_distance, -aux_column_width, v_one_half], [v_one_half, -aux_column_width, v_one_half], [z_max_r, z_max_r, v_one_half], - [separator_distance, aux_column_width, v_one_half], + [sep_distance, aux_column_width, v_one_half], [v_one_half, aux_column_width, v_one_half], ] ) @@ -1249,12 +1402,12 @@ def create_tile( [ [-aux_column_width, -v_one_half, -v_one_half], [aux_column_width, -v_one_half, -v_one_half], - [-aux_column_width, -separator_distance, -v_one_half], - [aux_column_width, -separator_distance, -v_one_half], + [-aux_column_width, -sep_distance, -v_one_half], + [aux_column_width, -sep_distance, -v_one_half], [-z_min_r, -z_min_r, -v_one_half], [z_min_r, -z_min_r, -v_one_half], - [-aux_column_width, -v_one_half, -separator_distance], - [aux_column_width, -v_one_half, -separator_distance], + [-aux_column_width, -v_one_half, -sep_distance], + [aux_column_width, -v_one_half, -sep_distance], [-aux_column_width, -hd_center, -hd_center], [aux_column_width, -hd_center, -hd_center], [-aux_z_min, -aux_z_min, -hd_center], @@ -1275,16 +1428,16 @@ def create_tile( [ [-z_min_r, z_min_r, -v_one_half], [z_min_r, z_min_r, -v_one_half], - [-aux_column_width, separator_distance, -v_one_half], - [aux_column_width, separator_distance, -v_one_half], + [-aux_column_width, sep_distance, -v_one_half], + [aux_column_width, sep_distance, -v_one_half], [-aux_column_width, v_one_half, -v_one_half], [aux_column_width, v_one_half, -v_one_half], [-aux_z_min, aux_z_min, -hd_center], [aux_z_min, aux_z_min, -hd_center], [-aux_column_width, hd_center, -hd_center], [aux_column_width, hd_center, -hd_center], - [-aux_column_width, v_one_half, -separator_distance], - [aux_column_width, v_one_half, -separator_distance], + [-aux_column_width, v_one_half, -sep_distance], + [aux_column_width, v_one_half, -sep_distance], [-center_r, center_r, -center_r], [center_r, center_r, -center_r], [-aux_y_max, hd_center, -aux_y_max], @@ -1305,16 +1458,16 @@ def create_tile( [aux_y_min, -hd_center, aux_y_min], [-center_r, -center_r, center_r], [center_r, -center_r, center_r], - [-aux_column_width, -v_one_half, separator_distance], - [aux_column_width, -v_one_half, separator_distance], + [-aux_column_width, -v_one_half, sep_distance], + [aux_column_width, -v_one_half, sep_distance], [-aux_column_width, -hd_center, hd_center], [aux_column_width, -hd_center, hd_center], [-aux_z_max, -aux_z_max, hd_center], [aux_z_max, -aux_z_max, hd_center], [-aux_column_width, -v_one_half, v_one_half], [aux_column_width, -v_one_half, v_one_half], - [-aux_column_width, -separator_distance, v_one_half], - [aux_column_width, -separator_distance, v_one_half], + [-aux_column_width, -sep_distance, v_one_half], + [aux_column_width, -sep_distance, v_one_half], [-z_max_r, -z_max_r, v_one_half], [z_max_r, -z_max_r, v_one_half], ] @@ -1335,12 +1488,12 @@ def create_tile( [aux_z_max, aux_z_max, hd_center], [-aux_column_width, hd_center, hd_center], [aux_column_width, hd_center, hd_center], - [-aux_column_width, v_one_half, separator_distance], - [aux_column_width, v_one_half, separator_distance], + [-aux_column_width, v_one_half, sep_distance], + [aux_column_width, v_one_half, sep_distance], [-z_max_r, z_max_r, v_one_half], [z_max_r, z_max_r, v_one_half], - [-aux_column_width, separator_distance, v_one_half], - [aux_column_width, separator_distance, v_one_half], + [-aux_column_width, sep_distance, v_one_half], + [aux_column_width, sep_distance, v_one_half], [-aux_column_width, v_one_half, v_one_half], [aux_column_width, v_one_half, v_one_half], ] @@ -1352,39 +1505,39 @@ def create_tile( _np.array( [ [-v_one_half, -v_one_half, -v_one_half], - [-separator_distance, -v_one_half, -v_one_half], + [-sep_distance, -v_one_half, -v_one_half], [-aux_column_width, -v_one_half, -v_one_half], - [-v_one_half, -separator_distance, -v_one_half], + [-v_one_half, -sep_distance, -v_one_half], [ - -separator_distance, - -separator_distance, + -sep_distance, + -sep_distance, -v_one_half, ], - [-aux_column_width, -separator_distance, -v_one_half], + [-aux_column_width, -sep_distance, -v_one_half], [-v_one_half, -aux_column_width, -v_one_half], - [-separator_distance, -aux_column_width, -v_one_half], + [-sep_distance, -aux_column_width, -v_one_half], [-z_min_r, -z_min_r, -v_one_half], - [-v_one_half, -v_one_half, -separator_distance], + [-v_one_half, -v_one_half, -sep_distance], [ - -separator_distance, + -sep_distance, -v_one_half, - -separator_distance, + -sep_distance, ], - [-aux_column_width, -v_one_half, -separator_distance], + [-aux_column_width, -v_one_half, -sep_distance], [ -v_one_half, - -separator_distance, - -separator_distance, + -sep_distance, + -sep_distance, ], [-hd_center, -hd_center, -hd_center], [-aux_column_width, -hd_center, -hd_center], - [-v_one_half, -aux_column_width, -separator_distance], + [-v_one_half, -aux_column_width, -sep_distance], [-hd_center, -aux_column_width, -hd_center], [-aux_z_min, -aux_z_min, -hd_center], [-v_one_half, -v_one_half, -aux_column_width], - [-separator_distance, -v_one_half, -aux_column_width], + [-sep_distance, -v_one_half, -aux_column_width], [-y_min_r, -v_one_half, -y_min_r], - [-v_one_half, -separator_distance, -aux_column_width], + [-v_one_half, -sep_distance, -aux_column_width], [-hd_center, -hd_center, -aux_column_width], [-aux_y_min, -hd_center, -aux_y_min], [-v_one_half, -x_min_r, -x_min_r], @@ -1399,29 +1552,29 @@ def create_tile( _np.array( [ [aux_column_width, -v_one_half, -v_one_half], - [separator_distance, -v_one_half, -v_one_half], + [sep_distance, -v_one_half, -v_one_half], [v_one_half, -v_one_half, -v_one_half], - [aux_column_width, -separator_distance, -v_one_half], - [separator_distance, -separator_distance, -v_one_half], - [v_one_half, -separator_distance, -v_one_half], + [aux_column_width, -sep_distance, -v_one_half], + [sep_distance, -sep_distance, -v_one_half], + [v_one_half, -sep_distance, -v_one_half], [z_min_r, -z_min_r, -v_one_half], - [separator_distance, -aux_column_width, -v_one_half], + [sep_distance, -aux_column_width, -v_one_half], [v_one_half, -aux_column_width, -v_one_half], - [aux_column_width, -v_one_half, -separator_distance], - [separator_distance, -v_one_half, -separator_distance], - [v_one_half, -v_one_half, -separator_distance], + [aux_column_width, -v_one_half, -sep_distance], + [sep_distance, -v_one_half, -sep_distance], + [v_one_half, -v_one_half, -sep_distance], [aux_column_width, -hd_center, -hd_center], [hd_center, -hd_center, -hd_center], - [v_one_half, -separator_distance, -separator_distance], + [v_one_half, -sep_distance, -sep_distance], [aux_z_min, -aux_z_min, -hd_center], [hd_center, -aux_column_width, -hd_center], - [v_one_half, -aux_column_width, -separator_distance], + [v_one_half, -aux_column_width, -sep_distance], [y_min_r, -v_one_half, -y_min_r], - [separator_distance, -v_one_half, -aux_column_width], + [sep_distance, -v_one_half, -aux_column_width], [v_one_half, -v_one_half, -aux_column_width], [aux_y_min, -hd_center, -aux_y_min], [hd_center, -hd_center, -aux_column_width], - [v_one_half, -separator_distance, -aux_column_width], + [v_one_half, -sep_distance, -aux_column_width], [center_r, -center_r, -center_r], [hd_center, -aux_x_max, -aux_x_max], [v_one_half, -x_max_r, -x_max_r], @@ -1434,31 +1587,31 @@ def create_tile( _np.array( [ [-v_one_half, aux_column_width, -v_one_half], - [-separator_distance, aux_column_width, -v_one_half], + [-sep_distance, aux_column_width, -v_one_half], [-z_min_r, z_min_r, -v_one_half], - [-v_one_half, separator_distance, -v_one_half], - [-separator_distance, separator_distance, -v_one_half], - [-aux_column_width, separator_distance, -v_one_half], + [-v_one_half, sep_distance, -v_one_half], + [-sep_distance, sep_distance, -v_one_half], + [-aux_column_width, sep_distance, -v_one_half], [-v_one_half, v_one_half, -v_one_half], - [-separator_distance, v_one_half, -v_one_half], + [-sep_distance, v_one_half, -v_one_half], [-aux_column_width, v_one_half, -v_one_half], - [-v_one_half, aux_column_width, -separator_distance], + [-v_one_half, aux_column_width, -sep_distance], [-hd_center, aux_column_width, -hd_center], [-aux_z_min, aux_z_min, -hd_center], - [-v_one_half, separator_distance, -separator_distance], + [-v_one_half, sep_distance, -sep_distance], [-hd_center, hd_center, -hd_center], [-aux_column_width, hd_center, -hd_center], - [-v_one_half, v_one_half, -separator_distance], - [-separator_distance, v_one_half, -separator_distance], - [-aux_column_width, v_one_half, -separator_distance], + [-v_one_half, v_one_half, -sep_distance], + [-sep_distance, v_one_half, -sep_distance], + [-aux_column_width, v_one_half, -sep_distance], [-v_one_half, x_min_r, -x_min_r], [-hd_center, aux_x_min, -aux_x_min], [-center_r, center_r, -center_r], - [-v_one_half, separator_distance, -aux_column_width], + [-v_one_half, sep_distance, -aux_column_width], [-hd_center, hd_center, -aux_column_width], [-aux_y_max, hd_center, -aux_y_max], [-v_one_half, v_one_half, -aux_column_width], - [-separator_distance, v_one_half, -aux_column_width], + [-sep_distance, v_one_half, -aux_column_width], [-y_max_r, v_one_half, -y_max_r], ] ) @@ -1469,31 +1622,31 @@ def create_tile( _np.array( [ [z_min_r, z_min_r, -v_one_half], - [separator_distance, aux_column_width, -v_one_half], + [sep_distance, aux_column_width, -v_one_half], [v_one_half, aux_column_width, -v_one_half], - [aux_column_width, separator_distance, -v_one_half], - [separator_distance, separator_distance, -v_one_half], - [v_one_half, separator_distance, -v_one_half], + [aux_column_width, sep_distance, -v_one_half], + [sep_distance, sep_distance, -v_one_half], + [v_one_half, sep_distance, -v_one_half], [aux_column_width, v_one_half, -v_one_half], - [separator_distance, v_one_half, -v_one_half], + [sep_distance, v_one_half, -v_one_half], [v_one_half, v_one_half, -v_one_half], [aux_z_min, aux_z_min, -hd_center], [hd_center, aux_column_width, -hd_center], - [v_one_half, aux_column_width, -separator_distance], + [v_one_half, aux_column_width, -sep_distance], [aux_column_width, hd_center, -hd_center], [hd_center, hd_center, -hd_center], - [v_one_half, separator_distance, -separator_distance], - [aux_column_width, v_one_half, -separator_distance], - [separator_distance, v_one_half, -separator_distance], - [v_one_half, v_one_half, -separator_distance], + [v_one_half, sep_distance, -sep_distance], + [aux_column_width, v_one_half, -sep_distance], + [sep_distance, v_one_half, -sep_distance], + [v_one_half, v_one_half, -sep_distance], [center_r, center_r, -center_r], [hd_center, aux_x_max, -aux_x_max], [v_one_half, x_max_r, -x_max_r], [aux_y_max, hd_center, -aux_y_max], [hd_center, hd_center, -aux_column_width], - [v_one_half, separator_distance, -aux_column_width], + [v_one_half, sep_distance, -aux_column_width], [y_max_r, v_one_half, -y_max_r], - [separator_distance, v_one_half, -aux_column_width], + [sep_distance, v_one_half, -aux_column_width], [v_one_half, v_one_half, -aux_column_width], ] ) @@ -1504,31 +1657,31 @@ def create_tile( _np.array( [ [-v_one_half, -v_one_half, aux_column_width], - [-separator_distance, -v_one_half, aux_column_width], + [-sep_distance, -v_one_half, aux_column_width], [-y_min_r, -v_one_half, y_min_r], - [-v_one_half, -separator_distance, aux_column_width], + [-v_one_half, -sep_distance, aux_column_width], [-hd_center, -hd_center, aux_column_width], [-aux_y_min, -hd_center, aux_y_min], [-v_one_half, -x_min_r, x_min_r], [-hd_center, -aux_x_min, aux_x_min], [-center_r, -center_r, center_r], - [-v_one_half, -v_one_half, separator_distance], - [-separator_distance, -v_one_half, separator_distance], - [-aux_column_width, -v_one_half, separator_distance], - [-v_one_half, -separator_distance, separator_distance], + [-v_one_half, -v_one_half, sep_distance], + [-sep_distance, -v_one_half, sep_distance], + [-aux_column_width, -v_one_half, sep_distance], + [-v_one_half, -sep_distance, sep_distance], [-hd_center, -hd_center, hd_center], [-aux_column_width, -hd_center, hd_center], - [-v_one_half, -aux_column_width, separator_distance], + [-v_one_half, -aux_column_width, sep_distance], [-hd_center, -aux_column_width, hd_center], [-aux_z_max, -aux_z_max, hd_center], [-v_one_half, -v_one_half, v_one_half], - [-separator_distance, -v_one_half, v_one_half], + [-sep_distance, -v_one_half, v_one_half], [-aux_column_width, -v_one_half, v_one_half], - [-v_one_half, -separator_distance, v_one_half], - [-separator_distance, -separator_distance, v_one_half], - [-aux_column_width, -separator_distance, v_one_half], + [-v_one_half, -sep_distance, v_one_half], + [-sep_distance, -sep_distance, v_one_half], + [-aux_column_width, -sep_distance, v_one_half], [-v_one_half, -aux_column_width, v_one_half], - [-separator_distance, -aux_column_width, v_one_half], + [-sep_distance, -aux_column_width, v_one_half], [-z_max_r, -z_max_r, v_one_half], ] ) @@ -1539,31 +1692,31 @@ def create_tile( _np.array( [ [y_min_r, -v_one_half, y_min_r], - [separator_distance, -v_one_half, aux_column_width], + [sep_distance, -v_one_half, aux_column_width], [v_one_half, -v_one_half, aux_column_width], [aux_y_min, -hd_center, aux_y_min], [hd_center, -hd_center, aux_column_width], - [v_one_half, -separator_distance, aux_column_width], + [v_one_half, -sep_distance, aux_column_width], [center_r, -center_r, center_r], [hd_center, -aux_x_max, aux_x_max], [v_one_half, -x_max_r, x_max_r], - [aux_column_width, -v_one_half, separator_distance], - [separator_distance, -v_one_half, separator_distance], - [v_one_half, -v_one_half, separator_distance], + [aux_column_width, -v_one_half, sep_distance], + [sep_distance, -v_one_half, sep_distance], + [v_one_half, -v_one_half, sep_distance], [aux_column_width, -hd_center, hd_center], [hd_center, -hd_center, hd_center], - [v_one_half, -separator_distance, separator_distance], + [v_one_half, -sep_distance, sep_distance], [aux_z_max, -aux_z_max, hd_center], [hd_center, -aux_column_width, hd_center], - [v_one_half, -aux_column_width, separator_distance], + [v_one_half, -aux_column_width, sep_distance], [aux_column_width, -v_one_half, v_one_half], - [separator_distance, -v_one_half, v_one_half], + [sep_distance, -v_one_half, v_one_half], [v_one_half, -v_one_half, v_one_half], - [aux_column_width, -separator_distance, v_one_half], - [separator_distance, -separator_distance, v_one_half], - [v_one_half, -separator_distance, v_one_half], + [aux_column_width, -sep_distance, v_one_half], + [sep_distance, -sep_distance, v_one_half], + [v_one_half, -sep_distance, v_one_half], [z_max_r, -z_max_r, v_one_half], - [separator_distance, -aux_column_width, v_one_half], + [sep_distance, -aux_column_width, v_one_half], [v_one_half, -aux_column_width, v_one_half], ] ) @@ -1576,29 +1729,29 @@ def create_tile( [-v_one_half, x_min_r, x_min_r], [-hd_center, aux_x_min, aux_x_min], [-center_r, center_r, center_r], - [-v_one_half, separator_distance, aux_column_width], + [-v_one_half, sep_distance, aux_column_width], [-hd_center, hd_center, aux_column_width], [-aux_y_max, hd_center, aux_y_max], [-v_one_half, v_one_half, aux_column_width], - [-separator_distance, v_one_half, aux_column_width], + [-sep_distance, v_one_half, aux_column_width], [-y_max_r, v_one_half, y_max_r], - [-v_one_half, aux_column_width, separator_distance], + [-v_one_half, aux_column_width, sep_distance], [-hd_center, aux_column_width, hd_center], [-aux_z_max, aux_z_max, hd_center], - [-v_one_half, separator_distance, separator_distance], + [-v_one_half, sep_distance, sep_distance], [-hd_center, hd_center, hd_center], [-aux_column_width, hd_center, hd_center], - [-v_one_half, v_one_half, separator_distance], - [-separator_distance, v_one_half, separator_distance], - [-aux_column_width, v_one_half, separator_distance], + [-v_one_half, v_one_half, sep_distance], + [-sep_distance, v_one_half, sep_distance], + [-aux_column_width, v_one_half, sep_distance], [-v_one_half, aux_column_width, v_one_half], - [-separator_distance, aux_column_width, v_one_half], + [-sep_distance, aux_column_width, v_one_half], [-z_max_r, z_max_r, v_one_half], - [-v_one_half, separator_distance, v_one_half], - [-separator_distance, separator_distance, v_one_half], - [-aux_column_width, separator_distance, v_one_half], + [-v_one_half, sep_distance, v_one_half], + [-sep_distance, sep_distance, v_one_half], + [-aux_column_width, sep_distance, v_one_half], [-v_one_half, v_one_half, v_one_half], - [-separator_distance, v_one_half, v_one_half], + [-sep_distance, v_one_half, v_one_half], [-aux_column_width, v_one_half, v_one_half], ] ) @@ -1613,27 +1766,27 @@ def create_tile( [v_one_half, x_max_r, x_max_r], [aux_y_max, hd_center, aux_y_max], [hd_center, hd_center, aux_column_width], - [v_one_half, separator_distance, aux_column_width], + [v_one_half, sep_distance, aux_column_width], [y_max_r, v_one_half, y_max_r], - [separator_distance, v_one_half, aux_column_width], + [sep_distance, v_one_half, aux_column_width], [v_one_half, v_one_half, aux_column_width], [aux_z_max, aux_z_max, hd_center], [hd_center, aux_column_width, hd_center], - [v_one_half, aux_column_width, separator_distance], + [v_one_half, aux_column_width, sep_distance], [aux_column_width, hd_center, hd_center], [hd_center, hd_center, hd_center], - [v_one_half, separator_distance, separator_distance], - [aux_column_width, v_one_half, separator_distance], - [separator_distance, v_one_half, separator_distance], - [v_one_half, v_one_half, separator_distance], + [v_one_half, sep_distance, sep_distance], + [aux_column_width, v_one_half, sep_distance], + [sep_distance, v_one_half, sep_distance], + [v_one_half, v_one_half, sep_distance], [z_max_r, z_max_r, v_one_half], - [separator_distance, aux_column_width, v_one_half], + [sep_distance, aux_column_width, v_one_half], [v_one_half, aux_column_width, v_one_half], - [aux_column_width, separator_distance, v_one_half], - [separator_distance, separator_distance, v_one_half], - [v_one_half, separator_distance, v_one_half], + [aux_column_width, sep_distance, v_one_half], + [sep_distance, sep_distance, v_one_half], + [v_one_half, sep_distance, v_one_half], [aux_column_width, v_one_half, v_one_half], - [separator_distance, v_one_half, v_one_half], + [sep_distance, v_one_half, v_one_half], [v_one_half, v_one_half, v_one_half], ] ) @@ -1671,5 +1824,4 @@ def create_tile( splines = spline_list.copy() else: derivatives.append(spline_list) - - return splines, derivatives + return (splines, derivatives) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index edaf0b477..339efd219 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -189,11 +189,7 @@ def test_tile_derivatives(np_rng, heps=1e-8, n_test_points=4): Number of testing points in the parametric domain """ # TODO: right now EllipsVoid, CubeVoid and InverseCross show wrong derivatives - skip_classes = [ - ms.tiles.EllipsVoid, - ms.tiles.CubeVoid, - ms.tiles.InverseCross3D, - ] + skip_classes = [ms.tiles.EllipsVoid, ms.tiles.CubeVoid] for tile_class in all_tile_classes: # TODO: right now skip classes with faultily implemented derivatives @@ -273,7 +269,7 @@ def test_tile_derivatives(np_rng, heps=1e-8, n_test_points=4): ): assert np.allclose(deriv_orig, deriv_fd), ( "Implemented derivative calculation for tile class" - + f"{tile_class}, parameter " + + f"{tile_class}, with closure {closure}, parameter " + f"{i_parameter+1}/{n_info_per_eval_point} at patch " + f"{i_patch+1}/{n_patches} does not match the derivative " + "obtained using Finite Differences" From 27d9e280443c251c5c85405fde2b075102c60c1c Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 9 Oct 2024 16:53:32 +0200 Subject: [PATCH 092/171] Fix CubeVoid derivatives --- splinepy/microstructure/tiles/cube_void.py | 2 +- tests/test_microstructure.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/splinepy/microstructure/tiles/cube_void.py b/splinepy/microstructure/tiles/cube_void.py index c435b6a88..9eeb3a33b 100644 --- a/splinepy/microstructure/tiles/cube_void.py +++ b/splinepy/microstructure/tiles/cube_void.py @@ -64,7 +64,7 @@ def _rotation_matrix_y(self, angle): def _rotation_matrix_y_deriv(self, angle): cc, ss = _np.cos(angle), _np.sin(angle) - return _np.array([[-ss, 0, -cc], [0, 1, 0], [cc, 0, -ss]]) + return _np.array([[-ss, 0, -cc], [0, 0, 0], [cc, 0, -ss]]) def create_tile( self, diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 339efd219..009cbabf4 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -176,7 +176,7 @@ def test_tile_closure(): check_control_points(tile_patches) -def test_tile_derivatives(np_rng, heps=1e-8, n_test_points=4): +def test_tile_derivatives(np_rng, heps=1e-7, n_test_points=10): """Testing the correctness of the tile derivatives using Finite Differences. This includes every closure and no closure, every parameter and every patch by evaluating at random points and for random parameters. @@ -188,8 +188,8 @@ def test_tile_derivatives(np_rng, heps=1e-8, n_test_points=4): n_test_points: int Number of testing points in the parametric domain """ - # TODO: right now EllipsVoid, CubeVoid and InverseCross show wrong derivatives - skip_classes = [ms.tiles.EllipsVoid, ms.tiles.CubeVoid] + # TODO: right now EllipsVoid shows wrong derivatives + skip_classes = [ms.tiles.EllipsVoid] for tile_class in all_tile_classes: # TODO: right now skip classes with faultily implemented derivatives From 9a5396d3957b8a12ac3eee68564e0771c10f789e Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 9 Oct 2024 17:22:06 +0200 Subject: [PATCH 093/171] Fix Ellipsvoid derivatives --- splinepy/microstructure/tiles/ellips_v_oid.py | 2 +- tests/test_microstructure.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/splinepy/microstructure/tiles/ellips_v_oid.py b/splinepy/microstructure/tiles/ellips_v_oid.py index 84025d8bf..67f31b6fd 100644 --- a/splinepy/microstructure/tiles/ellips_v_oid.py +++ b/splinepy/microstructure/tiles/ellips_v_oid.py @@ -91,7 +91,7 @@ def _rotation_matrix_y(self, angle): def _rotation_matrix_y_deriv(self, angle): cc, ss = _np.cos(angle), _np.sin(angle) - return _np.array([[-ss, 0, -cc], [0, 1, 0], [cc, 0, -ss]]) + return _np.array([[-ss, 0, -cc], [0, 0, 0], [cc, 0, -ss]]) def create_tile( self, diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 009cbabf4..260ee2fa2 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -188,14 +188,8 @@ def test_tile_derivatives(np_rng, heps=1e-7, n_test_points=10): n_test_points: int Number of testing points in the parametric domain """ - # TODO: right now EllipsVoid shows wrong derivatives - skip_classes = [ms.tiles.EllipsVoid] for tile_class in all_tile_classes: - # TODO: right now skip classes with faultily implemented derivatives - if tile_class in skip_classes: - continue - tile_creator = tile_class() # Skip test if tile class has no implemented sensitivities if not tile_creator._sensitivities_implemented: From bff1b763ec7981c5e1def3ddc761366740e75b6e Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 10 Oct 2024 09:23:36 +0200 Subject: [PATCH 094/171] Change to more appropriate name test_microtiles --- tests/{test_microstructure.py => test_microtiles.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_microstructure.py => test_microtiles.py} (100%) diff --git a/tests/test_microstructure.py b/tests/test_microtiles.py similarity index 100% rename from tests/test_microstructure.py rename to tests/test_microtiles.py From 879fc8e7327d98e018c68c3b60ed44e66a070214 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 10 Oct 2024 11:40:47 +0200 Subject: [PATCH 095/171] Add microstructure closure test --- tests/test_microstructure.py | 81 ++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test_microstructure.py diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py new file mode 100644 index 000000000..65cbc9c9f --- /dev/null +++ b/tests/test_microstructure.py @@ -0,0 +1,81 @@ +import splinepy.microstructure as ms +from splinepy.helpme.create import box +from splinepy.helpme.integrate import volume + +all_tile_classes = list(ms.tiles.everything().values()) + +TILING = [2, 2, 2] +BOX_DIMENSIONS = [1, 1, 1] +EPS = 1e-7 + +# TODO: the following tiles fail the closure test +CLOSURE_FAILS = [ms.tiles.HollowOctagonExtrude, ms.tiles.InverseCross3D] + + +def test_closing_face(): + def check_if_closed(multipatch, closure_direction): + """Helper function to see if multipatch has a closing surface + + Parameters + ---------- + multipatch: splinepy Multipatch + Microstructure multipatch + closure_direction: str + Direction in which the closure has been applied + """ + direction_index_dict = {"x": 0, "y": 1, "z": 2} + direction_index = direction_index_dict[closure_direction] + + def min_identifier(points): + return points[:, direction_index] < EPS + + def max_identifier(points): + return ( + points[:, direction_index] + > BOX_DIMENSIONS[direction_index] - EPS + ) + + multipatch.boundary_from_function(min_identifier, boundary_id=2) + multipatch.boundary_from_function(max_identifier, boundary_id=3) + + min_patches = multipatch.boundary_multipatch(2) + max_patches = multipatch.boundary_multipatch(3) + + # Check if the closing surface has a surface area of 1 + face_min_area = sum([volume(patch) for patch in min_patches.patches]) + face_max_area = sum([volume(patch) for patch in max_patches.patches]) + + assert ( + face_min_area > 1.0 - EPS + ), f"The closure of the {closure_direction}_min surface is not complete" + assert ( + face_max_area > 1.0 - EPS + ), f"The closure of the {closure_direction}_max surface is not complete" + + for tile_class in all_tile_classes: + # Skip tile if it doesn't support closure + if "_closure_directions" not in dir(tile_class): + continue + + # TODO: right now skip tiles which have faulty closures + if tile_class in CLOSURE_FAILS: + continue + + tile_creator = tile_class() + generator = ms.microstructure.Microstructure( + deformation_function=box(*BOX_DIMENSIONS[: tile_creator._dim]), + microtile=tile_creator, + tiling=TILING[: tile_creator._para_dim], + ) + # Go through all implemented closure direction + closure_directions = { + directionname[0] + for directionname in tile_creator._closure_directions + } + for closure_direction in closure_directions: + multipatch = generator.create(closing_face=closure_direction) + check_if_closed(multipatch, closure_direction) + + +def test_macro_sensitivities(): + pass From 0f45332ccdff2a1ff2c501c29092d09724451879 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 11 Oct 2024 15:46:35 +0200 Subject: [PATCH 096/171] Add test for microstructure macro sensitivities --- splinepy/microstructure/microstructure.py | 17 ++--- tests/test_microstructure.py | 76 ++++++++++++++++++++++- tests/test_microtiles.py | 6 +- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/splinepy/microstructure/microstructure.py b/splinepy/microstructure/microstructure.py index d93198f95..8bf9b79f0 100644 --- a/splinepy/microstructure/microstructure.py +++ b/splinepy/microstructure/microstructure.py @@ -428,8 +428,8 @@ def _compute_tiling_prerequisites( def create( self, closing_face=None, - knot_span_wise=None, - macro_sensitivities=None, + knot_span_wise=True, + macro_sensitivities=False, **kwargs, ): """Create a Microstructure. @@ -456,11 +456,12 @@ def create( if not self._sanity_check(): raise ValueError("Not enough information provided, abort") - # Set default values - if knot_span_wise is None: - knot_span_wise = True - if macro_sensitivities is None: - macro_sensitivities = False + assert isinstance( + knot_span_wise, bool + ), "knot_span_wise must be a bool" + assert isinstance( + macro_sensitivities, bool + ), "macro_senstivities must be a bool" # check if user wants closed structure if closing_face is not None: @@ -534,7 +535,7 @@ def create( if macro_sensitivities or parameter_sensitivities: spline_list_derivs = [ [] - for i in range( + for _ in range( n_parameter_sensitivities + n_macro_sensitivities ) ] diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 65cbc9c9f..b0b9ed916 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -1,3 +1,5 @@ +import numpy as np + import splinepy.microstructure as ms from splinepy.helpme.create import box from splinepy.helpme.integrate import volume @@ -13,6 +15,8 @@ def test_closing_face(): + """Check if closing face is working""" + def check_if_closed(multipatch, closure_direction): """Helper function to see if multipatch has a closing surface @@ -65,7 +69,7 @@ def max_identifier(points): generator = ms.microstructure.Microstructure( deformation_function=box(*BOX_DIMENSIONS[: tile_creator._dim]), microtile=tile_creator, - tiling=TILING[: tile_creator._para_dim], + tiling=TILING[: tile_creator._dim], ) # Go through all implemented closure direction closure_directions = { @@ -77,5 +81,71 @@ def max_identifier(points): check_if_closed(multipatch, closure_direction) -def test_macro_sensitivities(): - pass +def test_macro_sensitivities(np_rng, heps=1e-7, n_test_points=10): + """Testing the correctness of the derivatives of the whole microstructure w.r.t. + the deformation function's control points. It is tested by evaluating the derivative + obtained via finite differences. The values are evaluated at random points. + + Parameters + ---------- + heps: float + Perturbation size for finite difference evaluation + n_test_points: int + Number of testing points int the parametric domain""" + + for tile_class in all_tile_classes: + tile_creator = tile_class() + deformation_function_orig = box(*BOX_DIMENSIONS[: tile_creator._dim]) + generator = ms.microstructure.Microstructure( + deformation_function=deformation_function_orig, + microtile=tile_creator, + tiling=TILING[: tile_creator._dim], + ) + multipatch = generator.create(macro_sensitivities=True) + dim = multipatch.dim + n_cps = deformation_function_orig.cps.shape[0] + + # Set evaluation points as random spots in the parametric space + eval_points = np_rng.random((n_test_points, tile_creator._para_dim)) + microstructure_orig_evaluations = [ + patch.evaluate(eval_points) for patch in multipatch.patches + ] + n_patches = len(multipatch.patches) + + # Go through derivatives of every deformation function's control point + for ii_ctps in range(n_cps): + # Gradient through finite differences + deformation_function_perturbed = deformation_function_orig.copy() + deformation_function_perturbed.cps[ii_ctps, :] += heps + generator.deformation_function = deformation_function_perturbed + multipatch_perturbed = generator.create() + microstructure_perturbed_evaluations = [ + patch.evaluate(eval_points) + for patch in multipatch_perturbed.patches + ] + # Evaluate finite difference gradient + fd_sensitivity = [ + (patch_perturbed - patch_orig) / heps + for patch_perturbed, patch_orig in zip( + microstructure_orig_evaluations, + microstructure_perturbed_evaluations, + ) + ] + + # Go through each direction + for jj_dim in range(dim): + deriv_orig = multipatch.fields[ii_ctps * dim + jj_dim] + deriv_evaluations = [ + patch.evaluate(eval_points) for patch in deriv_orig.patches + ] + for k_patch, patch_deriv_implemented, patch_deriv_fd in zip( + range(n_patches), deriv_evaluations, fd_sensitivity + ): + assert np.allclose( + -patch_deriv_implemented[:, jj_dim], + patch_deriv_fd[:, jj_dim], + ), ( + "Implemented derivative calculation for tile class" + + f"{tile_class}, at patch {k_patch+1}/{n_patches} does not " + + "match the derivative obtained using Finite Differences" + ) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 260ee2fa2..87e306e39 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -76,7 +76,7 @@ def test_tile_class(): required_param in create_parameters ), f"create_tile() must have '{required_param}' as an input parameter" - # Ensure closure can be correctly handled + # Ensure closure can be handled correctly if "closure" in create_parameters: assert "_closure_directions" in members, ( "Tile class has closure ability. The available closure directions " @@ -130,7 +130,7 @@ def test_tile_bounds(): def test_tile_closure(): - """Check if closing tiles also lie in unit cube. TODO: also check derivatives.""" + """Check if closing tiles also lie in unit cube.""" for tile_class in all_tile_classes: # Skip tile if if does not support closure @@ -218,7 +218,7 @@ def test_tile_derivatives(np_rng, heps=1e-7, n_test_points=10): splines_orig, _ = tile_creator.create_tile( parameters=parameters, closure=closure ) - # Set evaluation points as 4 random spots in the parametric space + # Set evaluation points as random spots in the parametric space eval_points = np_rng.random( (n_test_points, splines_orig[0].para_dim) ) From 82222226958927cdbee3c6cf78e99009403ce597 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 11 Oct 2024 17:33:02 +0200 Subject: [PATCH 097/171] Add working closure to HollowOctagonExtrude --- .../tiles/hollow_octagon_extrude.py | 1254 +++++++++++++---- 1 file changed, 961 insertions(+), 293 deletions(-) diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index 16359ef49..1ad07f70f 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -29,6 +29,7 @@ def create_tile( parameters=None, parameter_sensitivities=None, contact_length=0.2, + closure=None, **kwargs, # noqa ARG002 ): """Create a microtile based on the parameters that describe the wall @@ -77,6 +78,15 @@ def create_tile( n_derivatives = 0 derivatives = None + if closure is not None: + return self._closing_tile( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + closure=closure, + contact_length=contact_length, + **kwargs, + ) + self.check_params(parameters) self.check_param_derivatives(parameter_sensitivities) @@ -108,9 +118,9 @@ def create_tile( right = _np.array( [ [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_zero], - [v_one, -v_outer_c_h + v_one_half, 0.0], - [v_h_void + v_one_half, v_inner_c_h + v_one_half, 0.0], - [v_one, v_outer_c_h + v_one_half, 0.0], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_zero], + [v_one, v_outer_c_h + v_one_half, v_zero], [v_h_void + v_one_half, -v_inner_c_h + v_one_half, v_one], [v_one, -v_outer_c_h + v_one_half, v_one], [v_h_void + v_one_half, v_inner_c_h + v_one_half, v_one], @@ -298,334 +308,992 @@ def _closing_tile( self.check_params(parameters) if parameter_sensitivities is not None: - raise NotImplementedError( - "Derivatives are not implemented for this tile yet" - ) + self.check_param_derivatives(parameter_sensitivities) + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None + # Check whether parameter is within bounds v_h_void = parameters[0, 0] - if not ((v_h_void > 0.01) and (v_h_void < 0.5)): + para_bound_lower, para_bound_upper = self._parameter_bounds[0] + if not ( + (v_h_void > para_bound_lower) and (v_h_void < para_bound_upper) + ): raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" + f"The thickness of the wall must be in ({para_bound_lower} and " + + f"{para_bound_upper})" ) - spline_list = [] - v_zero = 0.0 - v_one_half = 0.5 - v_one = 1.0 - v_outer_c_h = contact_length * 0.5 - v_inner_c_h = contact_length * parameters[0, 0] - - if closure == "x_min": - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) - - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) - - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) - - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) - - left = _np.array( - [ - [v_zero, v_zero], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_one], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + splines = [] + for i_derivative in range(n_derivatives + 1): + if i_derivative == 0: + v_zero = 0.0 + v_one_half = 0.5 + v_one = 1.0 + v_outer_c_h = contact_length * 0.5 + v_inner_c_h = contact_length * parameters[0, 0] + else: + v_h_void = parameter_sensitivities[0, 0, i_derivative - 1] + v_zero, v_one_half, v_one = [0.0, 0.0, 0.0] + v_outer_c_h = 0.0 + v_inner_c_h = contact_length * v_h_void - top_left = _np.array( - [ - [v_zero, v_one], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + spline_list = [] - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + if closure == "x_min": + # set points: + right = _np.array( + [ + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_outer_c_h + v_one_half, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, -v_outer_c_h + v_one_half, v_one], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_outer_c_h + v_one_half, v_one], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_outer_c_h + v_one_half, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_one, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_outer_c_h + v_one_half, v_one], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - elif closure == "x_max": - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, v_zero], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_one], - ] - ) + top = _np.array( + [ + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_one], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) + bottom_left = _np.array( + [ + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_zero, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, v_zero, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_zero, v_one], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) + left = _np.array( + [ + [v_zero, v_zero, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_one, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_zero, v_one], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, v_one, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) + top_left = _np.array( + [ + [v_zero, v_one, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_one, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_zero, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, -v_outer_c_h + v_one_half, v_one], + ] + ) - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + elif closure == "x_max": + right = _np.array( + [ + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_zero, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_one, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_zero, v_one], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_one, v_one], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, v_zero], - ] - ) + right_top = _np.array( + [ + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_one, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_one, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_one, v_one], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - elif closure == "y_min": - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) + top = _np.array( + [ + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - ] - ) + bottom_left = _np.array( + [ + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_zero, v_one], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - ] - ) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_zero, v_zero], - ] - ) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_zero, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [-v_outer_c_h + v_one_half, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_zero, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_zero, v_one], + ] + ) - bottom = _np.array( - [ - [v_one, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_zero, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + elif closure == "y_min": + # set points: + right = _np.array( + [ + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_outer_c_h + v_one_half, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, -v_outer_c_h + v_one_half, v_one], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_outer_c_h + v_one_half, v_one], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_one, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_outer_c_h + v_one_half, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_one, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_outer_c_h + v_one_half, v_one], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - elif closure == "y_max": - # set points: - right = _np.array( - [ - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - ] - ) + top = _np.array( + [ + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_one, v_one], + ] + ) - right_top = _np.array( - [ - [v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_one, v_outer_c_h + v_one_half], - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_one, v_one], - ] - ) + bottom_left = _np.array( + [ + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_zero, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [v_zero, v_zero, v_one], + ] + ) - top = _np.array( - [ - [v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_one, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - [v_zero, v_one], - ] - ) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + ] + ) - bottom_left = _np.array( - [ - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, -v_outer_c_h + v_one_half], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - ] - ) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + ] + ) - left = _np.array( - [ - [v_zero, -v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - ] - ) + bottom = _np.array( + [ + [v_one, v_zero, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_zero, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_one, v_zero, v_one], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [v_zero, v_zero, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + ] + ) - top_left = _np.array( - [ - [v_zero, v_outer_c_h + v_one_half], - [-v_h_void + v_one_half, v_inner_c_h + v_one_half], - [v_zero, v_one], - [-v_inner_c_h + v_one_half, v_h_void + v_one_half], - ] - ) + bottom_right = _np.array( + [ + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_one, v_zero, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [v_one, v_zero, v_one], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, -v_outer_c_h + v_one_half, v_one], + ] + ) - bottom = _np.array( - [ - [v_outer_c_h + v_one_half, v_zero], - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [-v_outer_c_h + v_one_half, v_zero], - [-v_inner_c_h + v_one_half, -v_h_void + v_one_half], - ] - ) + elif closure == "y_max": + # set points: + right = _np.array( + [ + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_outer_c_h + v_one_half, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, -v_outer_c_h + v_one_half, v_one], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_outer_c_h + v_one_half, v_one], + ] + ) - bottom_right = _np.array( - [ - [v_inner_c_h + v_one_half, -v_h_void + v_one_half], - [v_outer_c_h + v_one_half, v_zero], - [v_h_void + v_one_half, -v_inner_c_h + v_one_half], - [v_one, -v_outer_c_h + v_one_half], - ] - ) + right_top = _np.array( + [ + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, v_outer_c_h + v_one_half, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_one, v_one, v_zero], + [ + v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_one, v_outer_c_h + v_one_half, v_one], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_one, v_one, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right)) + top = _np.array( + [ + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_one, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_one, v_zero], + [ + v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_one, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + [v_zero, v_one, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=right_top)) + bottom_left = _np.array( + [ + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_zero, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom)) + left = _np.array( + [ + [v_zero, -v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, -v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=bottom_left)) + top_left = _np.array( + [ + [v_zero, v_outer_c_h + v_one_half, v_zero], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_zero, + ], + [v_zero, v_one, v_zero], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_zero, + ], + [v_zero, v_outer_c_h + v_one_half, v_one], + [ + -v_h_void + v_one_half, + v_inner_c_h + v_one_half, + v_one, + ], + [v_zero, v_one, v_one], + [ + -v_inner_c_h + v_one_half, + v_h_void + v_one_half, + v_one, + ], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=left)) + bottom = _np.array( + [ + [v_outer_c_h + v_one_half, v_zero, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [-v_outer_c_h + v_one_half, v_zero, v_zero], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [-v_outer_c_h + v_one_half, v_zero, v_one], + [ + -v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=top_left)) + bottom_right = _np.array( + [ + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_zero, + ], + [v_outer_c_h + v_one_half, v_zero, v_zero], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_zero, + ], + [v_one, -v_outer_c_h + v_one_half, v_zero], + [ + v_inner_c_h + v_one_half, + -v_h_void + v_one_half, + v_one, + ], + [v_outer_c_h + v_one_half, v_zero, v_one], + [ + v_h_void + v_one_half, + -v_inner_c_h + v_one_half, + v_one, + ], + [v_one, -v_outer_c_h + v_one_half, v_one], + ] + ) - spline_list.append(_Bezier(degrees=[1, 1], control_points=top)) + for control_points in [ + right, + right_top, + bottom, + bottom_left, + left, + top_left, + top, + bottom_right, + ]: + spline_list.append( + _Bezier(degrees=[1, 1, 1], control_points=control_points) + ) - spline_list.append( - _Bezier(degrees=[1, 1], control_points=bottom_right) - ) + if i_derivative == 0: + splines = spline_list.copy() + else: + derivatives.append(spline_list) - return (spline_list, None) + return (splines, derivatives) From 7c54ca1a5c9e509e5fcf81ea9828e90462e5ac20 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 14 Jan 2025 10:46:10 +0100 Subject: [PATCH 098/171] Add pytest parametrization of all the microtiles --- tests/test_microstructure.py | 161 ++++++++-------- tests/test_microtiles.py | 357 +++++++++++++++++------------------ 2 files changed, 258 insertions(+), 260 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index b0b9ed916..10c158bee 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -1,4 +1,5 @@ import numpy as np +from pytest import mark import splinepy.microstructure as ms from splinepy.helpme.create import box @@ -14,7 +15,8 @@ CLOSURE_FAILS = [ms.tiles.HollowOctagonExtrude, ms.tiles.InverseCross3D] -def test_closing_face(): +@mark.parametrize("tile_class", all_tile_classes) +def test_closing_face(tile_class): """Check if closing face is working""" def check_if_closed(multipatch, closure_direction): @@ -39,6 +41,7 @@ def max_identifier(points): > BOX_DIMENSIONS[direction_index] - EPS ) + # create min- and max-boundaries of the multipatch using the identifiers multipatch.boundary_from_function(min_identifier, boundary_id=2) multipatch.boundary_from_function(max_identifier, boundary_id=3) @@ -56,32 +59,31 @@ def max_identifier(points): face_max_area > 1.0 - EPS ), f"The closure of the {closure_direction}_max surface is not complete" - for tile_class in all_tile_classes: - # Skip tile if it doesn't support closure - if "_closure_directions" not in dir(tile_class): - continue - - # TODO: right now skip tiles which have faulty closures - if tile_class in CLOSURE_FAILS: - continue - - tile_creator = tile_class() - generator = ms.microstructure.Microstructure( - deformation_function=box(*BOX_DIMENSIONS[: tile_creator._dim]), - microtile=tile_creator, - tiling=TILING[: tile_creator._dim], - ) - # Go through all implemented closure direction - closure_directions = { - directionname[0] - for directionname in tile_creator._closure_directions - } - for closure_direction in closure_directions: - multipatch = generator.create(closing_face=closure_direction) - check_if_closed(multipatch, closure_direction) - - -def test_macro_sensitivities(np_rng, heps=1e-7, n_test_points=10): + # Skip tile if it doesn't support closure + if "_closure_directions" not in dir(tile_class): + return + + # TODO: right now skip tiles which have faulty closures + if tile_class in CLOSURE_FAILS: + return + + tile_creator = tile_class() + generator = ms.microstructure.Microstructure( + deformation_function=box(*BOX_DIMENSIONS[: tile_creator._dim]), + microtile=tile_creator, + tiling=TILING[: tile_creator._dim], + ) + # Go through all implemented closure direction + closure_directions = { + directionname[0] for directionname in tile_creator._closure_directions + } + for closure_direction in closure_directions: + multipatch = generator.create(closing_face=closure_direction) + check_if_closed(multipatch, closure_direction) + + +@mark.parametrize("tile_class", all_tile_classes) +def test_macro_sensitivities(tile_class, np_rng, heps=1e-7, n_test_points=10): """Testing the correctness of the derivatives of the whole microstructure w.r.t. the deformation function's control points. It is tested by evaluating the derivative obtained via finite differences. The values are evaluated at random points. @@ -93,59 +95,58 @@ def test_macro_sensitivities(np_rng, heps=1e-7, n_test_points=10): n_test_points: int Number of testing points int the parametric domain""" - for tile_class in all_tile_classes: - tile_creator = tile_class() - deformation_function_orig = box(*BOX_DIMENSIONS[: tile_creator._dim]) - generator = ms.microstructure.Microstructure( - deformation_function=deformation_function_orig, - microtile=tile_creator, - tiling=TILING[: tile_creator._dim], - ) - multipatch = generator.create(macro_sensitivities=True) - dim = multipatch.dim - n_cps = deformation_function_orig.cps.shape[0] - - # Set evaluation points as random spots in the parametric space - eval_points = np_rng.random((n_test_points, tile_creator._para_dim)) - microstructure_orig_evaluations = [ - patch.evaluate(eval_points) for patch in multipatch.patches + tile_creator = tile_class() + deformation_function_orig = box(*BOX_DIMENSIONS[: tile_creator._dim]) + generator = ms.microstructure.Microstructure( + deformation_function=deformation_function_orig, + microtile=tile_creator, + tiling=TILING[: tile_creator._dim], + ) + multipatch = generator.create(macro_sensitivities=True) + dim = multipatch.dim + n_cps = deformation_function_orig.cps.shape[0] + + # Set evaluation points as random spots in the parametric space + eval_points = np_rng.random((n_test_points, tile_creator._para_dim)) + microstructure_orig_evaluations = [ + patch.evaluate(eval_points) for patch in multipatch.patches + ] + n_patches = len(multipatch.patches) + + # Go through derivatives of every deformation function's control point + for ii_ctps in range(n_cps): + # Gradient through finite differences + deformation_function_perturbed = deformation_function_orig.copy() + deformation_function_perturbed.cps[ii_ctps, :] += heps + generator.deformation_function = deformation_function_perturbed + multipatch_perturbed = generator.create() + microstructure_perturbed_evaluations = [ + patch.evaluate(eval_points) + for patch in multipatch_perturbed.patches ] - n_patches = len(multipatch.patches) - - # Go through derivatives of every deformation function's control point - for ii_ctps in range(n_cps): - # Gradient through finite differences - deformation_function_perturbed = deformation_function_orig.copy() - deformation_function_perturbed.cps[ii_ctps, :] += heps - generator.deformation_function = deformation_function_perturbed - multipatch_perturbed = generator.create() - microstructure_perturbed_evaluations = [ - patch.evaluate(eval_points) - for patch in multipatch_perturbed.patches + # Evaluate finite difference gradient + fd_sensitivity = [ + (patch_perturbed - patch_orig) / heps + for patch_perturbed, patch_orig in zip( + microstructure_orig_evaluations, + microstructure_perturbed_evaluations, + ) + ] + + # Go through each direction + for jj_dim in range(dim): + deriv_orig = multipatch.fields[ii_ctps * dim + jj_dim] + deriv_evaluations = [ + patch.evaluate(eval_points) for patch in deriv_orig.patches ] - # Evaluate finite difference gradient - fd_sensitivity = [ - (patch_perturbed - patch_orig) / heps - for patch_perturbed, patch_orig in zip( - microstructure_orig_evaluations, - microstructure_perturbed_evaluations, + for k_patch, patch_deriv_implemented, patch_deriv_fd in zip( + range(n_patches), deriv_evaluations, fd_sensitivity + ): + assert np.allclose( + -patch_deriv_implemented[:, jj_dim], + patch_deriv_fd[:, jj_dim], + ), ( + "Implemented derivative calculation for tile class" + + f"{tile_class}, at patch {k_patch+1}/{n_patches} does not " + + "match the derivative obtained using Finite Differences" ) - ] - - # Go through each direction - for jj_dim in range(dim): - deriv_orig = multipatch.fields[ii_ctps * dim + jj_dim] - deriv_evaluations = [ - patch.evaluate(eval_points) for patch in deriv_orig.patches - ] - for k_patch, patch_deriv_implemented, patch_deriv_fd in zip( - range(n_patches), deriv_evaluations, fd_sensitivity - ): - assert np.allclose( - -patch_deriv_implemented[:, jj_dim], - patch_deriv_fd[:, jj_dim], - ), ( - "Implemented derivative calculation for tile class" - + f"{tile_class}, at patch {k_patch+1}/{n_patches} does not " - + "match the derivative obtained using Finite Differences" - ) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 87e306e39..92544484f 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -1,6 +1,7 @@ from inspect import getfullargspec import numpy as np +from pytest import mark import splinepy.microstructure as ms from splinepy.utils.data import cartesian_product as _cartesian_product @@ -17,7 +18,8 @@ def check_control_points(tile_patches): - """Check if all of tile's control points all lie within unit square/cube""" + """Helper function. Check if all of tile's control points all lie within unit + square/cube""" # Go through all patches for tile_patch in tile_patches: cps = tile_patch.control_points @@ -27,8 +29,8 @@ def check_control_points(tile_patches): def make_bounds_feasible(bounds): - """Bounds are given are understood as open set of min. and max. values. Therefore, - convert bounds to open set of these values. + """Helper function. Bounds are understood as open set of min. and max. values. + Therefore, convert bounds to open set of these values. Parameters ------------ @@ -46,7 +48,8 @@ def make_bounds_feasible(bounds): return np.array(feasible_bounds) -def test_tile_class(): +@mark.parametrize("tile_class", all_tile_classes) +def test_tile_class(tile_class): """Checks if all tile classes have the appropriate members and functions.""" required_class_variables = { "_para_dim": int, @@ -58,125 +61,122 @@ def test_tile_class(): "_parameters_shape": tuple, } - # Go through all available tiles - for tile_class in all_tile_classes: - # Get tile class' objects - members = [ - attr for attr in dir(tile_class) if not attr.startswith("__") - ] - - # Class must have function create_tile() + # Get tile class' objects + members = [attr for attr in dir(tile_class) if not attr.startswith("__")] + + # Class must have function create_tile() + assert hasattr( + tile_class, "create_tile" + ), "Tile class must have create_tile()" + # Tile must be able to take parameters and sensitivities as input + create_parameters = getfullargspec(tile_class.create_tile).args + for required_param in ["parameters", "parameter_sensitivities"]: + assert ( + required_param in create_parameters + ), f"create_tile() must have '{required_param}' as an input parameter" + + # Ensure closure can be handled correctly + if "closure" in create_parameters: + assert "_closure_directions" in members, ( + "Tile class has closure ability. The available closure directions " + + "are missing" + ) assert hasattr( - tile_class, "create_tile" - ), "Tile class must have create_tile()" - # Tile must be able to take parameters and sensitivities as input - create_parameters = getfullargspec(tile_class.create_tile).args - for required_param in ["parameters", "parameter_sensitivities"]: - assert ( - required_param in create_parameters - ), f"create_tile() must have '{required_param}' as an input parameter" - - # Ensure closure can be handled correctly - if "closure" in create_parameters: - assert "_closure_directions" in members, ( - "Tile class has closure ability. The available closure directions " - + "are missing" - ) - assert hasattr( - tile_class, "_closing_tile" - ), "Tile class has closure ability but no _closing_tile() function!" + tile_class, "_closing_tile" + ), "Tile class has closure ability but no _closing_tile() function!" - # Check if tile class has all required variables and they are the correct type - for required_variable, var_type in required_class_variables.items(): - assert ( - required_variable in members - ), f"Tile class needs to have member variable '{required_variable}'" - assert isinstance( - eval(f"tile_class.{required_variable}"), var_type - ), f"Variable {required_variable} needs to be of type {var_type}" + # Check if tile class has all required variables and they are the correct type + for required_variable, var_type in required_class_variables.items(): + assert ( + required_variable in members + ), f"Tile class needs to have member variable '{required_variable}'" + assert isinstance( + eval(f"tile_class.{required_variable}"), var_type + ), f"Variable {required_variable} needs to be of type {var_type}" -def test_tile_bounds(): +@mark.parametrize("tile_class", all_tile_classes) +def test_tile_bounds(tile_class): """Test if tile is still in unit cube at the bounds. Checks default and also non-default parameter values.""" - for tile_class in all_tile_classes: - tile_creator = tile_class() - # Create tile with default parameters - tile_patches, _ = tile_creator.create_tile() + tile_creator = tile_class() + # Create tile with default parameters + tile_patches, _ = tile_creator.create_tile() + check_control_points(tile_patches) + + # Skip certain classes for testing + skip_tile_class = False + for skip_tile in skip_tiles: + if tile_class is skip_tile: + skip_tile_class = True + break + if skip_tile_class: + return + + # Go through all extremes of parameters and ensure that also they create + # tiles within unit square/cube + feasible_parameter_bounds = make_bounds_feasible( + tile_creator._parameter_bounds + ) + all_parameter_bounds = _cartesian_product(feasible_parameter_bounds) + for parameter_extremes in all_parameter_bounds: + tile_patches, _ = tile_creator.create_tile( + parameters=parameter_extremes.reshape( + tile_creator._parameters_shape + ) + ) check_control_points(tile_patches) - # Skip certain classes for testing - skip_tile_class = False - for skip_tile in skip_tiles: - if tile_class is skip_tile: - skip_tile_class = True - break - if skip_tile_class: - continue - - # Go through all extremes of parameters and ensure that also they create - # tiles within unit square/cube - feasible_parameter_bounds = make_bounds_feasible( - tile_creator._parameter_bounds + +@mark.parametrize("tile_class", all_tile_classes) +def test_tile_closure(tile_class): + """Check if closing tiles also lie in unit cube.""" + + # Skip tile if if does not support closure + if "_closure_directions" not in dir(tile_class): + return + tile_creator = tile_class() + # Go through all implemented closure directions + for closure_direction in tile_creator._closure_directions: + tile_patches, sensitivities = tile_creator.create_tile( + closure=closure_direction ) - all_parameter_bounds = _cartesian_product(feasible_parameter_bounds) + assert ( + sensitivities is None + ), "Ensure sensitivities for closure are None if no sensitivities are asked" + + check_control_points(tile_patches) + + # Also check non-default parameters + # Skip certain classes for testing + skip_tile_class = False + for skip_tile in skip_tiles: + if tile_class is skip_tile: + skip_tile_class = True + break + if skip_tile_class: + return + + # Go through all extremes of parameters and ensure that also they create + # tiles within unit square/cube + feasible_parameter_bounds = make_bounds_feasible( + tile_creator._parameter_bounds + ) + all_parameter_bounds = _cartesian_product(feasible_parameter_bounds) + # Go through all implemented closure directions + for closure_direction in tile_creator._closure_directions: for parameter_extremes in all_parameter_bounds: tile_patches, _ = tile_creator.create_tile( parameters=parameter_extremes.reshape( tile_creator._parameters_shape - ) + ), + closure=closure_direction, ) check_control_points(tile_patches) -def test_tile_closure(): - """Check if closing tiles also lie in unit cube.""" - - for tile_class in all_tile_classes: - # Skip tile if if does not support closure - if "_closure_directions" not in dir(tile_class): - continue - tile_creator = tile_class() - # Go through all implemented closure directions - for closure_direction in tile_creator._closure_directions: - tile_patches, sensitivities = tile_creator.create_tile( - closure=closure_direction - ) - assert ( - sensitivities is None - ), "Ensure sensitivities for closure are None if no sensitivities are asked" - - check_control_points(tile_patches) - - # Also check non-default parameters - # Skip certain classes for testing - skip_tile_class = False - for skip_tile in skip_tiles: - if tile_class is skip_tile: - skip_tile_class = True - break - if skip_tile_class: - continue - - # Go through all extremes of parameters and ensure that also they create - # tiles within unit square/cube - feasible_parameter_bounds = make_bounds_feasible( - tile_creator._parameter_bounds - ) - all_parameter_bounds = _cartesian_product(feasible_parameter_bounds) - # Go through all implemented closure directions - for closure_direction in tile_creator._closure_directions: - for parameter_extremes in all_parameter_bounds: - tile_patches, _ = tile_creator.create_tile( - parameters=parameter_extremes.reshape( - tile_creator._parameters_shape - ), - closure=closure_direction, - ) - check_control_points(tile_patches) - - -def test_tile_derivatives(np_rng, heps=1e-7, n_test_points=10): +@mark.parametrize("tile_class", all_tile_classes) +def test_tile_derivatives(tile_class, np_rng, heps=1e-7, n_test_points=10): """Testing the correctness of the tile derivatives using Finite Differences. This includes every closure and no closure, every parameter and every patch by evaluating at random points and for random parameters. @@ -189,82 +189,79 @@ def test_tile_derivatives(np_rng, heps=1e-7, n_test_points=10): Number of testing points in the parametric domain """ - for tile_class in all_tile_classes: - tile_creator = tile_class() - # Skip test if tile class has no implemented sensitivities - if not tile_creator._sensitivities_implemented: - continue - - # Choose random parameter(s) within bounds - parameter_bounds = np.array(tile_creator._parameter_bounds) - parameters = parameter_bounds[:, 0] + np_rng.random( - len(parameter_bounds) - ) * np.ptp(parameter_bounds, axis=1) - parameters = parameters.reshape(tile_creator._parameters_shape) - - # Test no closure as well as ... - closure_directions = [None] - # ... every closure implemented - if "_closure_directions" in dir(tile_creator): - closure_directions += tile_creator._closure_directions - - # Retrieve shape values of parameters - n_eval_points = tile_creator._evaluation_points.shape[0] - n_info_per_eval_point = tile_creator._n_info_per_eval_point - - # Test each closure direction - for closure in closure_directions: - # Evaluate tile with given parameter and closure configuration - splines_orig, _ = tile_creator.create_tile( - parameters=parameters, closure=closure + tile_creator = tile_class() + # Skip test if tile class has no implemented sensitivities + if not tile_creator._sensitivities_implemented: + return + + # Choose random parameter(s) within bounds + parameter_bounds = np.array(tile_creator._parameter_bounds) + parameters = parameter_bounds[:, 0] + np_rng.random( + len(parameter_bounds) + ) * np.ptp(parameter_bounds, axis=1) + parameters = parameters.reshape(tile_creator._parameters_shape) + + # Test no closure as well as ... + closure_directions = [None] + # ... every closure implemented + if "_closure_directions" in dir(tile_creator): + closure_directions += tile_creator._closure_directions + + # Retrieve shape values of parameters + n_eval_points = tile_creator._evaluation_points.shape[0] + n_info_per_eval_point = tile_creator._n_info_per_eval_point + + # Test each closure direction + for closure in closure_directions: + # Evaluate tile with given parameter and closure configuration + splines_orig, _ = tile_creator.create_tile( + parameters=parameters, closure=closure + ) + # Set evaluation points as random spots in the parametric space + eval_points = np_rng.random((n_test_points, splines_orig[0].para_dim)) + splines_orig_evaluations = [ + spl.evaluate(eval_points) for spl in splines_orig + ] + # Go through all the parameters individually + for i_parameter in range(n_info_per_eval_point): + # Get derivatives w.r.t. one parameter + parameter_sensitivities = np.zeros( + (n_eval_points, n_info_per_eval_point, 1) ) - # Set evaluation points as random spots in the parametric space - eval_points = np_rng.random( - (n_test_points, splines_orig[0].para_dim) + parameter_sensitivities[:, i_parameter, :] = 1 + _, derivatives = tile_creator.create_tile( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + closure=closure, ) - splines_orig_evaluations = [ - spl.evaluate(eval_points) for spl in splines_orig + deriv_evaluations = [ + deriv.evaluate(eval_points) for deriv in derivatives[0] ] - # Go through all the parameters individually - for i_parameter in range(n_info_per_eval_point): - # Get derivatives w.r.t. one parameter - parameter_sensitivities = np.zeros( - (n_eval_points, n_info_per_eval_point, 1) - ) - parameter_sensitivities[:, i_parameter, :] = 1 - _, derivatives = tile_creator.create_tile( - parameters=parameters, - parameter_sensitivities=parameter_sensitivities, - closure=closure, + # Perform finite difference evaluation + parameters_perturbed = parameters.copy() + parameters_perturbed[:, i_parameter] += heps + splines_perturbed, _ = tile_creator.create_tile( + parameters=parameters_perturbed, closure=closure + ) + spline_perturbed_evaluations = [ + spl.evaluate(eval_points) for spl in splines_perturbed + ] + # Evaluate finite difference gradient + fd_sensitivities = [ + (spl_pert - spl_orig) / heps + for spl_pert, spl_orig in zip( + spline_perturbed_evaluations, splines_orig_evaluations ) - deriv_evaluations = [ - deriv.evaluate(eval_points) for deriv in derivatives[0] - ] - # Perform finite difference evaluation - parameters_perturbed = parameters.copy() - parameters_perturbed[:, i_parameter] += heps - splines_perturbed, _ = tile_creator.create_tile( - parameters=parameters_perturbed, closure=closure + ] + # Check every patch + n_patches = len(fd_sensitivities) + for i_patch, deriv_orig, deriv_fd in zip( + range(n_patches), deriv_evaluations, fd_sensitivities + ): + assert np.allclose(deriv_orig, deriv_fd), ( + "Implemented derivative calculation for tile class" + + f"{tile_class}, with closure {closure}, parameter " + + f"{i_parameter+1}/{n_info_per_eval_point} at patch " + + f"{i_patch+1}/{n_patches} does not match the derivative " + + "obtained using Finite Differences" ) - spline_perturbed_evaluations = [ - spl.evaluate(eval_points) for spl in splines_perturbed - ] - # Evaluate finite difference gradient - fd_sensitivities = [ - (spl_pert - spl_orig) / heps - for spl_pert, spl_orig in zip( - spline_perturbed_evaluations, splines_orig_evaluations - ) - ] - # Check every patch - n_patches = len(fd_sensitivities) - for i_patch, deriv_orig, deriv_fd in zip( - range(n_patches), deriv_evaluations, fd_sensitivities - ): - assert np.allclose(deriv_orig, deriv_fd), ( - "Implemented derivative calculation for tile class" - + f"{tile_class}, with closure {closure}, parameter " - + f"{i_parameter+1}/{n_info_per_eval_point} at patch " - + f"{i_patch+1}/{n_patches} does not match the derivative " - + "obtained using Finite Differences" - ) From 03dc05aa8789bce5181a0afa3f27444668b3a7b4 Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 15 Jan 2025 10:03:02 +0100 Subject: [PATCH 099/171] Add to description that tiles are not tested --- splinepy/microstructure/tiles/ellips_v_oid.py | 3 +++ splinepy/microstructure/tiles/snappy.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/splinepy/microstructure/tiles/ellips_v_oid.py b/splinepy/microstructure/tiles/ellips_v_oid.py index 67f31b6fd..878adcc2b 100644 --- a/splinepy/microstructure/tiles/ellips_v_oid.py +++ b/splinepy/microstructure/tiles/ellips_v_oid.py @@ -7,6 +7,9 @@ class EllipsVoid(_TileBase): """Void in form of an ellipsoid set into a unit cell. + TODO: Currently this tile is skipped for testing, since the control points easily + lie outside of the unit cube, even though the tile itself lies within it + The Ellips(v)oid :D Parametrization acts on the elipsoid's orientation as well as on its diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index 84eba670d..ab521f7b7 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -8,6 +8,9 @@ class Snappy(_TileBase): """Snap-through tile consisting of a thin truss and a thick truss that collide into each other. + # TODO: currently the tile parameters are not implemented as the parameters variable + therefore, no parameter sensitivities are calculated + .. raw:: html

Fullscreen.

From d73e51c99bd279570a94db7be6076cae783ba777 Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 15 Jan 2025 10:03:55 +0100 Subject: [PATCH 100/171] Simplify code which skips tile classes --- tests/test_microtiles.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 92544484f..87a725dc3 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -105,12 +105,7 @@ def test_tile_bounds(tile_class): check_control_points(tile_patches) # Skip certain classes for testing - skip_tile_class = False - for skip_tile in skip_tiles: - if tile_class is skip_tile: - skip_tile_class = True - break - if skip_tile_class: + if tile_class in skip_tiles: return # Go through all extremes of parameters and ensure that also they create @@ -149,12 +144,7 @@ def test_tile_closure(tile_class): # Also check non-default parameters # Skip certain classes for testing - skip_tile_class = False - for skip_tile in skip_tiles: - if tile_class is skip_tile: - skip_tile_class = True - break - if skip_tile_class: + if tile_class in skip_tiles: return # Go through all extremes of parameters and ensure that also they create From 3254e72b34d378c46dbc3d508715556094dc2320 Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 15 Jan 2025 10:08:56 +0100 Subject: [PATCH 101/171] Failed sensitivity calculation now outputs evaluation points --- tests/test_microstructure.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 10c158bee..54096da1d 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -148,5 +148,7 @@ def test_macro_sensitivities(tile_class, np_rng, heps=1e-7, n_test_points=10): ), ( "Implemented derivative calculation for tile class" + f"{tile_class}, at patch {k_patch+1}/{n_patches} does not " - + "match the derivative obtained using Finite Differences" + + "match the derivative obtained using Finite Differences at " + + "the following evaluation points:\n" + + str(eval_points) ) From 7a9c7e1e7c2e53464b7b0f5467f85d5dc628f73b Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 15 Jan 2025 13:29:04 +0100 Subject: [PATCH 102/171] Fixing RNG seed, sensitivity step size and number of random test points --- tests/conftest.py | 19 +++++++++++++++++- tests/test_microstructure.py | 19 ++++++++++++++---- tests/test_microtiles.py | 39 ++++++++++++++++++++++++++++-------- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b4381441a..ff0e84315 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ def error_log(*args): @pytest.fixture def np_rng(): - return np.random.default_rng() + return np.random.default_rng(seed=0) # query points @@ -41,6 +41,23 @@ def queries_3D(): ] +# hard-coded values to keep the same for derivative/sensitivity calculations +@pytest.fixture +def heps(): + """ + Perturbation/step size for finite difference evaluation of derivative/sensitivity + """ + return 1e-7 + + +@pytest.fixture +def n_test_points(): + """ + Number of random testing points (in parametric domain) + """ + return 10 + + # initializing a spline should be a test itself, so provide `dict_spline` # this is "iga-book"'s fig 2.15. @pytest.fixture diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 54096da1d..8e28ac08a 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -17,7 +17,13 @@ @mark.parametrize("tile_class", all_tile_classes) def test_closing_face(tile_class): - """Check if closing face is working""" + """Check if closing face is working + + Parameters + --------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile + """ def check_if_closed(multipatch, closure_direction): """Helper function to see if multipatch has a closing surface @@ -83,17 +89,22 @@ def max_identifier(points): @mark.parametrize("tile_class", all_tile_classes) -def test_macro_sensitivities(tile_class, np_rng, heps=1e-7, n_test_points=10): +def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): """Testing the correctness of the derivatives of the whole microstructure w.r.t. the deformation function's control points. It is tested by evaluating the derivative obtained via finite differences. The values are evaluated at random points. Parameters ---------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile + np_rng: numpy.random._generator.Generator + Default random number generator heps: float - Perturbation size for finite difference evaluation + Perturbation size for finite difference evaluation. Defined in conftest.py n_test_points: int - Number of testing points int the parametric domain""" + Number of testing points int the parametric domain. Defined in conftest.py + """ tile_creator = tile_class() deformation_function_orig = box(*BOX_DIMENSIONS[: tile_creator._dim]) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 87a725dc3..3f1c8c75f 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -50,7 +50,13 @@ def make_bounds_feasible(bounds): @mark.parametrize("tile_class", all_tile_classes) def test_tile_class(tile_class): - """Checks if all tile classes have the appropriate members and functions.""" + """Checks if all tile classes have the appropriate members and functions. + + Parameters + --------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile + """ required_class_variables = { "_para_dim": int, "_dim": int, @@ -98,7 +104,13 @@ def test_tile_class(tile_class): @mark.parametrize("tile_class", all_tile_classes) def test_tile_bounds(tile_class): """Test if tile is still in unit cube at the bounds. Checks default and also - non-default parameter values.""" + non-default parameter values. + + Parameters + --------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile + """ tile_creator = tile_class() # Create tile with default parameters tile_patches, _ = tile_creator.create_tile() @@ -125,7 +137,13 @@ def test_tile_bounds(tile_class): @mark.parametrize("tile_class", all_tile_classes) def test_tile_closure(tile_class): - """Check if closing tiles also lie in unit cube.""" + """Check if closing tiles also lie in unit cube. + + Parameters + --------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile + """ # Skip tile if if does not support closure if "_closure_directions" not in dir(tile_class): @@ -166,19 +184,22 @@ def test_tile_closure(tile_class): @mark.parametrize("tile_class", all_tile_classes) -def test_tile_derivatives(tile_class, np_rng, heps=1e-7, n_test_points=10): +def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): """Testing the correctness of the tile derivatives using Finite Differences. This includes every closure and no closure, every parameter and every patch by evaluating at random points and for random parameters. Parameters --------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile + np_rng: numpy.random._generator.Generator + Default random number generator heps: float - Perturbation size for finite difference evaluation + Perturbation size for finite difference evaluation. Defined in conftest.py n_test_points: int - Number of testing points in the parametric domain + Number of testing points in the parametric domain. Defined in conftest.py """ - tile_creator = tile_class() # Skip test if tile class has no implemented sensitivities if not tile_creator._sensitivities_implemented: @@ -253,5 +274,7 @@ def test_tile_derivatives(tile_class, np_rng, heps=1e-7, n_test_points=10): + f"{tile_class}, with closure {closure}, parameter " + f"{i_parameter+1}/{n_info_per_eval_point} at patch " + f"{i_patch+1}/{n_patches} does not match the derivative " - + "obtained using Finite Differences" + + "obtained using Finite Differences at the following evaluation " + + "points:\n" + + str(eval_points) ) From e2a56a2263fd447cdeed63b8f496891ca7ccc17e Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 15 Jan 2025 13:42:28 +0100 Subject: [PATCH 103/171] Fix failing knot vector test --- tests/test_knot_vectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_knot_vectors.py b/tests/test_knot_vectors.py index a16c2fdd3..9d9af3457 100644 --- a/tests/test_knot_vectors.py +++ b/tests/test_knot_vectors.py @@ -32,4 +32,4 @@ def test_knot_vectors(splinetype, request): spline.knot_vectors, strict=True, ): - assert np.allclose(np.array(spl_kv), np.repeat(u_kv, kn_m)) + assert np.allclose(spl_kv.numpy(), np.repeat(u_kv, kn_m)) From 432e58494d1e61b6a80df3ef5e038fc852c81ae7 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 16 Jan 2025 15:22:39 +0100 Subject: [PATCH 104/171] Refactor, add common code to TileBase --- splinepy/microstructure/tiles/tile_base.py | 189 ++++++++++++++++++++- tests/test_microstructure.py | 2 +- tests/test_microtiles.py | 4 +- 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 0c4a64100..5529a286b 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -12,6 +12,11 @@ class TileBase(_SplinepyBase): _para_dim = None _evaluation_points = None _n_info_per_eval_point = None + _sensitivities_implemented = None + _closure_directions = None + _parameter_bounds = None + _parameters_shape = None + _default_parameter_value = None def __init__(self): if type(self) is TileBase: @@ -46,7 +51,21 @@ def evaluation_points(self): return self._raise_if_not_set_else_return("_evaluation_points") @property - def dim(self): + def n_info_per_eval_point(cls): + """Number of parameters per evaluation point + + Parameters + ---------- + None + + Returns + ------- + n_info : int + """ + return cls._raise_if_not_set_else_return("_n_info_per_eval_point") + + @property + def dim(cls): """Returns dimensionality in physical space of the Microtile. Parameters @@ -73,8 +92,79 @@ def para_dim(self): """ return self._raise_if_not_set_else_return("_para_dim") - def check_params(self, params): - """Checks if the parameters have the correct format and shape + @property + def sensitivites_implemented(cls): + """Returns whether sensitivities are implemented for the microtile + + Parameters + ---------- + None + + Returns + ------- + is_implemented: bool + """ + return cls._raise_if_not_set_else_return("_sensitivities_implemented") + + @property + def closure_directions(self): + """Returns the available closure directions of the microtile + + Parameters + ---------- + None + + Returns + ------- + directions: None/list + """ + return self._closure_directions + + @property + def parameter_bounds(cls): + """Returns the bounds for the microtiles' parameters + + Parameters + ---------- + None + + Returns + ------- + bounds: list> + """ + return cls._raise_if_not_set_else_return("_parameter_bounds") + + @property + def parameters_shape(cls): + """Returns the shape of the microtile's parameters array + + Parameters + ---------- + None + + Returns + ------- + shape: tuple + """ + return cls._raise_if_not_set_else_return("_parameters_shape") + + @property + def default_parameter_value(cls): + """Returns the default value of the microtile's parameters + + Parameters + ---------- + None + + Returns + ------- + default_value: float/None + """ + return cls._default_parameter_value + + def check_params(self, parameters): + """Checks if the parameters have the correct format and shape and are within + defined bounds Parameters ---------- @@ -85,14 +175,14 @@ def check_params(self, params): ------- True: Boolean """ - # check if tuple - - if not (isinstance(params, _np.ndarray) and params.ndim == 2): + # Check correct format + if not (isinstance(parameters, _np.ndarray) and parameters.ndim == 2): raise TypeError("parameters must be two-dimensional np array") + # Check correct shape if not ( - (self._evaluation_points.shape[0] == params.shape[0]) - and (self._n_info_per_eval_point == params.shape[1]) + (self._evaluation_points.shape[0] == parameters.shape[0]) + and (self._n_info_per_eval_point == parameters.shape[1]) ): raise TypeError( f"Mismatch in parameter size, expected " @@ -100,6 +190,20 @@ def check_params(self, params): f"{self._n_info_per_eval_point}" ) + # Check if all parameters are within bounds + if self._parameter_bounds is not None: + bounds = _np.array(self._parameter_bounds) + lower_bounds = bounds[:, 0] + upper_bounds = bounds[:, 1] + within_bounds = (parameters.ravel() > lower_bounds) & ( + parameters.ravel() < upper_bounds + ) + if not _np.all(within_bounds): + raise ValueError( + f"The parameters {parameters} must be within the following bounds: " + + f"lower: {lower_bounds} and upper: {upper_bounds}" + ) + return True def check_param_derivatives(self, derivatives): @@ -153,3 +257,72 @@ def create_tile(self, **kwargs): raise NotImplementedError( f"create_tile() not implemented for {type(self)}" ) + + def _process_input(self, parameters, parameter_sensitivities): + """Processing input for create_tile and _closing_tile + + Parameters + ------------- + parameters: np.ndarray + Tile parameters + parameter_sensitivities: np.ndarray + Tile parameter sensitivities to be calculated + + Returns + --------- + parameters: np.ndarray + Tile parameters + n_derivatives: np.ndarray + Number of different derivatives to compute + derivatives: list/None + Initialized list of derivatives + """ + # Set parameters to default values if not user-given + if parameters is None: + default_value = self.default_parameter_value + self._logd( + f"Setting parameters to default values ({default_value})" + ) + if isinstance(default_value, float): + parameters = _np.full( + (len(self.evaluation_points), self.n_info_per_eval_point), + default_value, + ) + elif isinstance(default_value, _np.ndarray): + parameters = default_value + + # Initialize list of derivatives + if parameter_sensitivities is not None: + n_derivatives = parameter_sensitivities.shape[2] + derivatives = [] + else: + n_derivatives = 0 + derivatives = None + + # Validity check of parameters and their sensitivities + self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) + + return parameters, n_derivatives, derivatives + + def _check_custom_parameter(self, value, param_name, min_bound, max_bound): + """Check if a custom tile parameter (e.g. contact length) is within bounds + + Parameters + ------------------ + value: float + Value of the custom parameter + param_name: str + Name of custom parameter + min_bound: float + Lower bound for parameter + max_bound: float + Upper bound for parameter + """ + if not isinstance(value, float): + raise ValueError(f"Invalid type for {param_name}") + + if not ((value > min_bound) and (value < max_bound)): + raise ValueError( + f"{param_name} must be in ({min_bound}, {max_bound})" + ) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 8e28ac08a..10f6be9ce 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -66,7 +66,7 @@ def max_identifier(points): ), f"The closure of the {closure_direction}_max surface is not complete" # Skip tile if it doesn't support closure - if "_closure_directions" not in dir(tile_class): + if tile_class._closure_directions is None: return # TODO: right now skip tiles which have faulty closures diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 3f1c8c75f..e28bcc55e 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -146,7 +146,7 @@ def test_tile_closure(tile_class): """ # Skip tile if if does not support closure - if "_closure_directions" not in dir(tile_class): + if tile_class._closure_directions is None: return tile_creator = tile_class() # Go through all implemented closure directions @@ -215,7 +215,7 @@ def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): # Test no closure as well as ... closure_directions = [None] # ... every closure implemented - if "_closure_directions" in dir(tile_creator): + if tile_creator._closure_directions is not None: closure_directions += tile_creator._closure_directions # Retrieve shape values of parameters From 6d4e8e4cfd5bd796b4a30242e86c2e39dd544827 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 16 Jan 2025 15:25:01 +0100 Subject: [PATCH 105/171] Refactor all tile classes to make use of common functions --- splinepy/microstructure/tiles/armadillo.py | 71 ++++------------ splinepy/microstructure/tiles/chi.py | 32 ++------ splinepy/microstructure/tiles/cross_2d.py | 80 +++++-------------- splinepy/microstructure/tiles/cross_3d.py | 73 +++++------------ .../microstructure/tiles/cross_3d_linear.py | 73 +++++------------ splinepy/microstructure/tiles/cube_void.py | 18 ++--- .../microstructure/tiles/double_lattice.py | 35 +++----- splinepy/microstructure/tiles/ellips_v_oid.py | 15 ++-- splinepy/microstructure/tiles/hollow_cube.py | 26 ++---- .../microstructure/tiles/hollow_octagon.py | 70 +++------------- .../tiles/hollow_octagon_extrude.py | 79 ++++-------------- .../microstructure/tiles/inverse_cross_3d.py | 75 +++++------------ splinepy/microstructure/tiles/snappy.py | 5 +- 13 files changed, 153 insertions(+), 499 deletions(-) diff --git a/splinepy/microstructure/tiles/armadillo.py b/splinepy/microstructure/tiles/armadillo.py index e9fe2a50c..bbac762a9 100644 --- a/splinepy/microstructure/tiles/armadillo.py +++ b/splinepy/microstructure/tiles/armadillo.py @@ -31,6 +31,7 @@ class Armadillo(_TileBase): ] _parameter_bounds = [[0.0, 0.5]] _parameters_shape = (1, 1) + _default_parameter_value = 0.2 def _closing_tile( self, @@ -69,35 +70,14 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if not isinstance(contact_length, float): - raise ValueError("Invalid Type for radius") + self._check_custom_parameter( + contact_length, "contact length", 0.0, 0.99 + ) - if not ((contact_length > 0) and (contact_length < 0.99)): - raise ValueError("The length of a side must be in (0.01, 0.99)") - - if parameters is None: - self._logd("Setting parameters to default values (0.2)") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - - if parameter_sensitivities is not None: - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" - ) + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) splines = [] for i_derivative in range(n_derivatives + 1): @@ -5062,35 +5042,14 @@ def create_tile( derivative_list : list / None """ - if not isinstance(contact_length, float): - raise ValueError("Invalid Type for radius") + self._check_custom_parameter( + contact_length, "contact length", 0.0, 0.99 + ) - if not ((contact_length > 0) and (contact_length < 0.99)): - raise ValueError("The length of a side must be in (0.01, 0.99)") - - if parameters is None: - self._logd("Setting parameters to default values (0.2)") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - - if parameter_sensitivities is not None: - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" - ) + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) if closure is not None: return self._closing_tile( diff --git a/splinepy/microstructure/tiles/chi.py b/splinepy/microstructure/tiles/chi.py index 0bcd215e9..4a48132dd 100644 --- a/splinepy/microstructure/tiles/chi.py +++ b/splinepy/microstructure/tiles/chi.py @@ -21,6 +21,7 @@ class Chi(_TileBase): _sensitivities_implemented = True _parameter_bounds = [[-_np.pi / 2, _np.pi / 2]] _parameters_shape = (1, 1) + _default_parameter_value = _np.pi / 8 def create_tile( self, @@ -44,33 +45,10 @@ def create_tile( microtile_list : list(splines) """ - # set to default if nothing is given - if parameters is None: - self._logd( - "Tile request is not parametrized, setting default Pi/8" - ) - parameters = _np.array([[_np.pi / 8]]) - else: - angle_bounds = self._parameter_bounds[0] - if not ( - _np.all(parameters >= angle_bounds[0]) - and _np.all(parameters < angle_bounds[1]) - ): - error_message = ( - f"The parameter must be in {angle_bounds[0]}" - + f"and {angle_bounds[1]}" - ) - raise ValueError(error_message) - pass - self.check_params(parameters) - - # Check if user requests derivative splines - if self.check_param_derivatives(parameter_sensitivities): - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) splines = [] for i_derivative in range(n_derivatives + 1): diff --git a/splinepy/microstructure/tiles/cross_2d.py b/splinepy/microstructure/tiles/cross_2d.py index 2fe51c2be..b919ff0ad 100644 --- a/splinepy/microstructure/tiles/cross_2d.py +++ b/splinepy/microstructure/tiles/cross_2d.py @@ -32,6 +32,7 @@ class Cross2D(_TileBase): [0.0, 0.5] ] * 4 # valid for default center_expansion=1.0 _parameters_shape = (4, 1) + _default_parameter_value = 0.2 def _closing_tile( self, @@ -72,36 +73,17 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if parameters is None: - self._logd("Tile request is not parametrized, setting default 0.2") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - self.check_params(parameters) - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError("Thickness out of range (0, .5)") + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) - if not (0.0 < float(boundary_width) < 0.5): - raise ValueError("Boundary Width is out of range") - - if not (0.0 < float(filling_height) < 1.0): - raise ValueError("Filling must be in (0,1)") - - # Check if user requests derivative splines - if parameter_sensitivities is not None: - # Check format - self.check_param_derivatives(parameter_sensitivities) - - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + self._check_custom_parameter( + boundary_width, "boundary width", 0.0, 0.5 + ) + self._check_custom_parameter( + filling_height, "filling height", 0.0, 1.0 + ) splines = [] for i_derivative in range(n_derivatives + 1): @@ -420,43 +402,19 @@ def create_tile( derivative_list : list / None """ - if not isinstance(center_expansion, float): - raise ValueError("Invalid Type") + self._check_custom_parameter( + center_expansion, "center expansion", 0.5, 1.5 + ) - if not ((center_expansion > 0.5) and (center_expansion < 1.5)): - error_message = "Center Expansion must be in (0.5, 1.5)" - raise ValueError(error_message) + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) max_radius = min(0.5, (0.5 / center_expansion)) - - # set to default if nothing is given - if parameters is None: - self._logd("Setting branch thickness to default 0.2") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - self.check_params(parameters) - - if not (_np.all(parameters > 0) and _np.all(parameters < max_radius)): + if not _np.all(parameters < max_radius): raise ValueError(f"Thickness out of range (0, {max_radius})") - self.check_param_derivatives(parameter_sensitivities) - - # Check if user requests derivative splines - if parameter_sensitivities is not None: - # Check format - self.check_param_derivatives(parameter_sensitivities) - - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - # Closure requested, pass to function if closure is not None: return self._closing_tile( diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index fba595111..5552d998e 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -34,6 +34,7 @@ class Cross3D(_TileBase): [0.0, 0.5] ] * 6 # valid for default center_expansion=1.0 _parameters_shape = (6, 1) + _default_parameter_value = 0.2 def _closing_tile( self, @@ -74,32 +75,17 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if parameters is None: - self._logd("Tile request is not parametrized, setting default 0.2") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError("Thickness out of range (0, .5)") + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) - # Check if user requests derivative splines - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - if not (0.0 < float(boundary_width) < 0.5): - raise ValueError("Boundary Width is out of range") - - if not (0.0 < float(filling_height) < 1.0): - raise ValueError("Filling must be in (0,1)") + self._check_custom_parameter( + boundary_width, "boundary width", 0.0, 0.5 + ) + self._check_custom_parameter( + filling_height, "filling height", 0.0, 1.0 + ) splines = [] for i_derivative in range(n_derivatives + 1): @@ -524,44 +510,23 @@ def create_tile( derivative_list : list / None """ - if not isinstance(center_expansion, float): - raise ValueError("Invalid Type") + self._check_custom_parameter( + center_expansion, "center expansion", 0.5, 1.5 + ) - if not ((center_expansion > 0.5) and (center_expansion < 1.5)): - raise ValueError("Center Expansion must be in (.5,1.5)") + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) # Max radius, so there is no tanglement in the crosstile max_radius = min(0.5, (0.5 / center_expansion)) - - # set to default if nothing is given - if parameters is None: - self._logd("Setting branch thickness to default 0.2") - parameters = ( - _np.ones( - ( - self._evaluation_points.shape[0], - self._n_info_per_eval_point, - ) - ) - * 0.2 - ) - - # Check for type and consistency - self.check_params(parameters) - if _np.any(parameters <= 0) or _np.any(parameters > max_radius): + if _np.any(parameters > max_radius): raise ValueError( f"Radii must be in (0,{max_radius}) for " f"center_expansion {center_expansion}" ) - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - if closure is not None: if closure not in self._closure_directions: raise NotImplementedError( diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index 22c0f7959..103fa6372 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -34,6 +34,7 @@ class Cross3DLinear(_TileBase): [0.0, 0.5] ] * 6 # valid for default center_expansion=1.0 _parameters_shape = (6, 1) + _default_parameter_value = 0.2 def _closing_tile( self, @@ -74,32 +75,17 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if parameters is None: - self._logd("Tile request is not parametrized, setting default 0.2") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError("Thickness out of range (0, .5)") + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) - # Check if user requests derivative splines - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - if not (0.0 < float(boundary_width) < 0.5): - raise ValueError("Boundary Width is out of range") - - if not (0.0 < float(filling_height) < 1.0): - raise ValueError("Filling must be in (0,1)") + self._check_custom_parameter( + boundary_width, "boundary width", 0.0, 0.5 + ) + self._check_custom_parameter( + filling_height, "filling height", 0.0, 1.0 + ) splines = [] for i_derivative in range(n_derivatives + 1): @@ -480,44 +466,23 @@ def create_tile( derivative_list : list / None """ - if not isinstance(center_expansion, float): - raise ValueError("Invalid Type") + self._check_custom_parameter( + center_expansion, "center expansion", 0.5, 1.5 + ) - if not ((center_expansion > 0.5) and (center_expansion < 1.5)): - raise ValueError("Center Expansion must be in (.5,1.5)") + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) # Max radius, so there is no tanglement in the crosstile max_radius = min(0.5, (0.5 / center_expansion)) - - # set to default if nothing is given - if parameters is None: - self._logd("Setting branch thickness to default 0.2") - parameters = ( - _np.ones( - ( - self._evaluation_points.shape[0], - self._n_info_per_eval_point, - ) - ) - * 0.2 - ) - - # Check for type and consistency - self.check_params(parameters) - if _np.any(parameters <= 0) or _np.any(parameters > max_radius): + if _np.any(parameters > max_radius): raise ValueError( f"Radii must be in (0,{max_radius}) for " f"center_expansion {center_expansion}" ) - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - if closure is not None: return self._closing_tile( parameters=parameters, diff --git a/splinepy/microstructure/tiles/cube_void.py b/splinepy/microstructure/tiles/cube_void.py index 9eeb3a33b..0ac29e965 100644 --- a/splinepy/microstructure/tiles/cube_void.py +++ b/splinepy/microstructure/tiles/cube_void.py @@ -35,6 +35,7 @@ class CubeVoid(_TileBase): [-_np.pi / 2, _np.pi / 2], ] _parameters_shape = (1, 4) + _default_parameter_value = _np.array([[0.5, 0.5, 0.0, 0.0]]) # Aux values _sphere_ctps = _np.array( @@ -100,23 +101,16 @@ def create_tile( microtile_list : list(splines) derivatives : list> / None """ # set to default if nothing is given - if parameters is None: - parameters = _np.array([0.5, 0.5, 0, 0]).reshape( - self._parameters_shape - ) + + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) # Create center ellipsoid # RotY * RotX * DIAG(r_x, r_yz) * T_base [r_x, r_yz, rot_x, rot_y] = parameters.flatten() - # Check if user requests derivative splines - if self.check_param_derivatives(parameter_sensitivities): - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - # Prepare auxiliary matrices and values diag = _np.diag([r_x, r_yz, r_yz]) rotMx = self._rotation_matrix_x(rot_x) diff --git a/splinepy/microstructure/tiles/double_lattice.py b/splinepy/microstructure/tiles/double_lattice.py index e040078f9..d1fb1438a 100644 --- a/splinepy/microstructure/tiles/double_lattice.py +++ b/splinepy/microstructure/tiles/double_lattice.py @@ -23,6 +23,7 @@ class DoubleLattice(_TileBase): _sensitivities_implemented = True _parameter_bounds = [[0.0, 1 / (2 * (1 + _np.sqrt(2)))]] * 2 _parameters_shape = (1, 2) + _default_parameter_value = 0.1 def create_tile( self, @@ -53,32 +54,14 @@ def create_tile( ------- microtile_list : list(splines) """ - if not isinstance(contact_length, float): - raise ValueError("Invalid Type") - if not ((contact_length > 0.0) and (contact_length < 1.0)): - raise ValueError("Contact length must be in (0.,1.)") - - # set to default if nothing is given - if parameters is None: - self._logd("Tile request is not parametrized, setting default 0.2") - parameters = _np.ones((1, 2)) * 0.1 - if not ( - _np.all(parameters > 0) - and _np.all(parameters < 0.5 / (1 + _np.sqrt(2))) - ): - raise ValueError( - "Parameters must be between 0.01 and 0.5/(1+sqrt(2))=0.207" - ) - - self.check_params(parameters) - - # Check if user requests derivative splines - if self.check_param_derivatives(parameter_sensitivities): - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + self._check_custom_parameter( + contact_length, "contact length", 0.0, 1.0 + ) + + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) splines = [] for i_derivative in range(n_derivatives + 1): diff --git a/splinepy/microstructure/tiles/ellips_v_oid.py b/splinepy/microstructure/tiles/ellips_v_oid.py index 878adcc2b..37d12c669 100644 --- a/splinepy/microstructure/tiles/ellips_v_oid.py +++ b/splinepy/microstructure/tiles/ellips_v_oid.py @@ -41,6 +41,7 @@ class EllipsVoid(_TileBase): [-_np.pi / 2, _np.pi / 2], ] _parameters_shape = (1, 4) + _default_parameter_value = _np.array([[0.5, 0.5, 0, 0]]) # Aux values _c0 = 0.5 / 3**0.5 @@ -130,21 +131,15 @@ def create_tile( microtile_list : list(splines) derivatives: list> / None """ # set to default if nothing is given - if parameters is None: - parameters = _np.array([0.5, 0.5, 0, 0]).reshape(1, 1, 4) + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) # Create center ellipsoid # RotY * RotX * DIAG(r_x, r_yz) * T_base [r_x, r_yz, rot_x, rot_y] = parameters.flatten() - # Check if user requests derivative splines - if self.check_param_derivatives(parameter_sensitivities): - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - # Prepare auxiliary matrices and values diag = _np.diag([r_x, r_yz, r_yz]) rotMx = self._rotation_matrix_x(rot_x) diff --git a/splinepy/microstructure/tiles/hollow_cube.py b/splinepy/microstructure/tiles/hollow_cube.py index 36353d417..222124009 100644 --- a/splinepy/microstructure/tiles/hollow_cube.py +++ b/splinepy/microstructure/tiles/hollow_cube.py @@ -31,6 +31,7 @@ class HollowCube(_TileBase): _sensitivities_implemented = True _parameter_bounds = [[0.0, 0.5]] * 7 _parameters_shape = (7, 1) + _default_parameter_value = 0.2 def create_tile( self, @@ -56,27 +57,10 @@ def create_tile( ------- microtile_list : list(splines) """ - - if parameters is None: - self._logd("Setting parameters to default value (0.2)") - parameters = ( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - self.check_params(parameters) - - if not (_np.all(parameters > 0.0) and _np.all(parameters < 0.5)): - raise ValueError("The wall thickness must be in (0.0 and 0.5)") - - if self.check_param_derivatives(parameter_sensitivities): - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) splines = [] diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index 22ace6555..c1e3271ec 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -22,6 +22,7 @@ class HollowOctagon(_TileBase): _closure_directions = ["x_min", "x_max", "y_min", "y_max"] _parameter_bounds = [[0.0, 0.5]] _parameters_shape = (1, 1) + _default_parameter_value = 0.2 def _closing_tile( self, @@ -55,35 +56,12 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if parameters is None: - self._log("Tile request is not parametrized, setting default 0.2") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" - ) - - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - - if parameter_sensitivities is not None: - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) v_h_void = parameters[0, 0] - if not ((v_h_void > 0.0) and (v_h_void < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.0 and 0.5)" - ) splines = [] for i_derivative in range(n_derivatives + 1): @@ -454,36 +432,14 @@ def create_tile( microtile_list : list(splines) derivatives: list> / None """ - - if not isinstance(contact_length, float): - raise ValueError("Invalid Type for radius") - - if not ((contact_length > 0) and (contact_length < 0.99)): - raise ValueError("The length of a side must be in (0.01, 0.99)") - - if parameters is None: - self._logd("Setting parameters to default values (0.2)") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - if parameter_sensitivities is not None: - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" - ) + self._check_custom_parameter( + contact_length, "contact length", 0.0, 0.99 + ) + + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) if closure is not None: return self._closing_tile( diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index 1ad07f70f..6bcfd4313 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -19,10 +19,10 @@ class HollowOctagonExtrude(_TileBase): _evaluation_points = _np.array([[0.5, 0.5, 0.5]]) _n_info_per_eval_point = 1 _sensitivities_implemented = True - # TODO: closure in create_tile still missing _closure_directions = ["x_min", "x_max", "y_min", "y_max"] _parameter_bounds = [[0.0, 0.5]] _parameters_shape = (1, 1) + _default_parameter_value = 0.2 def create_tile( self, @@ -56,27 +56,16 @@ def create_tile( derivatives: list> / None """ - if not isinstance(contact_length, float): - raise ValueError("Invalid Type for radius") + self._check_custom_parameter( + contact_length, "contact length", 0.0, 0.99 + ) + # Process input + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) - if not ((contact_length > 0) and (contact_length < 0.99)): - raise ValueError("The length of a side must be in (0.01, 0.99)") - - if parameters is None: - self._logd("Setting parameters to default values (0.2)") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - if parameter_sensitivities is not None: - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + v_h_void = parameters[0, 0] if closure is not None: return self._closing_tile( @@ -87,15 +76,6 @@ def create_tile( **kwargs, ) - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - - v_h_void = parameters[0, 0] - if not ((v_h_void > 0.0) and (v_h_void < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.0 and 0.5)" - ) - splines = [] for i_derivative in range(n_derivatives + 1): if i_derivative == 0: @@ -259,7 +239,7 @@ def create_tile( def _closing_tile( self, parameters=None, - parameter_sensitivities=None, # TODO + parameter_sensitivities=None, contact_length=0.2, closure=None, ): @@ -291,40 +271,13 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if parameters is None: - self._log("Tile request is not parametrized, setting default 0.2") - parameters = _np.array( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - if not (_np.all(parameters[0] > 0) and _np.all(parameters[0] < 0.5)): - raise ValueError( - "The thickness of the wall must be in (0.01 and 0.49)" - ) - - self.check_params(parameters) + # Process input + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - # Check whether parameter is within bounds v_h_void = parameters[0, 0] - para_bound_lower, para_bound_upper = self._parameter_bounds[0] - if not ( - (v_h_void > para_bound_lower) and (v_h_void < para_bound_upper) - ): - raise ValueError( - f"The thickness of the wall must be in ({para_bound_lower} and " - + f"{para_bound_upper})" - ) splines = [] for i_derivative in range(n_derivatives + 1): diff --git a/splinepy/microstructure/tiles/inverse_cross_3d.py b/splinepy/microstructure/tiles/inverse_cross_3d.py index 4787cd3b2..5a366ee11 100644 --- a/splinepy/microstructure/tiles/inverse_cross_3d.py +++ b/splinepy/microstructure/tiles/inverse_cross_3d.py @@ -37,6 +37,7 @@ class InverseCross3D(_TileBase): _closure_directions = ["z_min", "z_max"] _parameter_bounds = [[0.2, 0.3]] * 6 # For default values _parameters_shape = (6, 1) + _default_parameter_value = 0.21 def _closing_tile( self, @@ -74,37 +75,17 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - if parameters is None: - self._logd("Tile request is not parametrized, setting default 0.2") - parameters = ( - _np.ones( - ( - self._evaluation_points.shape[0], - self._n_info_per_eval_point, - ) - ) - * 0.2 - ) - - self.check_params(parameters) - - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None - - if not (_np.all(parameters > 0) and _np.all(parameters < 0.5)): - raise ValueError("Thickness out of range (0, .5)") + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) - if not (0.0 < float(boundary_width) < 0.5): - raise ValueError("Boundary Width is out of range") - - if not (0.0 < float(filling_height) < 1.0): - raise ValueError("Filling must be in (0,1)") + self._check_custom_parameter( + boundary_width, "boundary width", 0.0, 0.5 + ) + self._check_custom_parameter( + filling_height, "filling height", 0.0, 1.0 + ) splines = [] @@ -1069,43 +1050,25 @@ def create_tile( microtile_list : list(splines) """ - if not isinstance(center_expansion, float): - raise ValueError("Invalid type for center_expansion") - - if not ((center_expansion > 0.5) and (center_expansion < 1.5)): - raise ValueError("Center Expansion must be in (.5, 1.5)") + self._check_custom_parameter( + center_expansion, "center expansion", 0.5, 1.5 + ) # Check if all radii are in allowed range max_radius = min(0.5, (0.5 / center_expansion)) max_radius = min(max_radius, separator_distance) min_radius = max(0.5 - separator_distance, 0) - # set to default if nothing is given - if parameters is None: - self._logd("Setting branch thickness to default 0.2") - parameters = ( - _np.ones( - (len(self._evaluation_points), self._n_info_per_eval_point) - ) - * 0.2 - ) - - self.check_params(parameters) - - if parameter_sensitivities is not None: - self.check_param_derivatives(parameter_sensitivities) - - n_derivatives = parameter_sensitivities.shape[2] - derivatives = [] - else: - n_derivatives = 0 - derivatives = None + parameters, n_derivatives, derivatives = self._process_input( + parameters=parameters, + parameter_sensitivities=parameter_sensitivities, + ) if _np.any(parameters < min_radius) or _np.any( parameters > max_radius ): raise ValueError( - f"Radii must be in (0,{max_radius}) for " + f"Radii must be in ({min_radius},{max_radius}) for " f"center_expansion {center_expansion}" ) diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index ab521f7b7..d3202aa63 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -353,8 +353,9 @@ def create_tile( if param < 0: raise ValueError("Invalid parameter, must be > 0.") - if not ((contact_length > 0) and (contact_length < 0.49)): - raise ValueError("The length of a side must be in (0.01, 0.49)") + self._check_custom_parameter( + contact_length, "contact length", 0.0, 0.49 + ) # Check horizontal parameters if not ((r + contact_length) < 0.5): From 7fc1d8aeb82eb2f3ac0960cbb6d4812e3c0e56e4 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 16 Jan 2025 18:01:58 +0100 Subject: [PATCH 106/171] Add comments according to coderabbitai --- tests/conftest.py | 9 ++++++++- tests/test_microstructure.py | 10 ++++++++++ tests/test_microtiles.py | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index ff0e84315..ef4b69ff2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,12 @@ def queries_3D(): @pytest.fixture def heps(): """ - Perturbation/step size for finite difference evaluation of derivative/sensitivity + Perturbation/step size for finite difference evaluation of derivative/sensitivity. + + The value 1e-7 is arbitrary, but is a a compromise between: + - Being small enough to ensure the finite difference calculation being accurate + enough + - Being large enough to avoid round-off error in floating-point arithmetic """ return 1e-7 @@ -54,6 +59,8 @@ def heps(): def n_test_points(): """ Number of random testing points (in parametric domain) + + The number 10 is arbitrary and should ensure to have good test coverage """ return 10 diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 10f6be9ce..e2fccc026 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -153,6 +153,12 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): for k_patch, patch_deriv_implemented, patch_deriv_fd in zip( range(n_patches), deriv_evaluations, fd_sensitivity ): + # Verify derivative shapes + assert patch_deriv_implemented.shape[1] == dim, ( + "The derivative at " + + f"patch {k_patch} has the wrong dimensions." + ) + # Assert correctness of sensitivity assert np.allclose( -patch_deriv_implemented[:, jj_dim], patch_deriv_fd[:, jj_dim], @@ -162,4 +168,8 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): + "match the derivative obtained using Finite Differences at " + "the following evaluation points:\n" + str(eval_points) + + "\nImplemented derivative:\n" + + str(patch_deriv_implemented[:, jj_dim]) + + "\nFinite difference derivative:\n" + + str(patch_deriv_fd[:, jj_dim]) ) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index e28bcc55e..5e37fd40a 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -277,4 +277,8 @@ def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): + "obtained using Finite Differences at the following evaluation " + "points:\n" + str(eval_points) + + "\nImplemented derivative:\n" + + str(deriv_orig) + + "\nFinite difference derivative:\n" + + str(deriv_fd) ) From 6c2d3cb483859aedff5772ec65ad39f752767a14 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 17 Jan 2025 18:02:20 +0100 Subject: [PATCH 107/171] Add check of default tile parameters --- tests/test_microtiles.py | 62 +++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 5e37fd40a..4355812d3 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -73,13 +73,14 @@ def test_tile_class(tile_class): # Class must have function create_tile() assert hasattr( tile_class, "create_tile" - ), "Tile class must have create_tile()" + ), f"Tile class {tile_class.__name__} must have create_tile() method" # Tile must be able to take parameters and sensitivities as input create_parameters = getfullargspec(tile_class.create_tile).args for required_param in ["parameters", "parameter_sensitivities"]: - assert ( - required_param in create_parameters - ), f"create_tile() must have '{required_param}' as an input parameter" + assert required_param in create_parameters, ( + f"{tile_class.__name__}.create_tile() must have '{required_param}' as an " + "input parameter" + ) # Ensure closure can be handled correctly if "closure" in create_parameters: @@ -100,6 +101,37 @@ def test_tile_class(tile_class): eval(f"tile_class.{required_variable}"), var_type ), f"Variable {required_variable} needs to be of type {var_type}" + # Check default parameter value if there is one + if tile_class._parameters_shape != (): + # Assert that there is a default value + assert hasattr(tile_class, "_default_parameter_value"), ( + f'{tile_class.__name__} must have "_default_parameter_value" as a class ' + " attribute." + ) + # Check the default value's type + default_value = tile_class._default_parameter_value + if isinstance(default_value, np.ndarray): + # Check the dimensions + assert default_value.shape == tile_class._parameters_shape, ( + f"Default parameter values for tile {tile_class.__name__} has the wrong" + " dimensions" + ) + # Check if default values are within bounds + default_value = default_value.ravel() + elif not isinstance(default_value, float): + raise ValueError( + f"Default parameter value for tile {tile_class.__name__} must either be" + "a float or a numpy array" + ) + + # Check if default values are within bounds + parameter_bounds = np.asarray(tile_class._parameter_bounds) + lower_bounds = parameter_bounds[:, 0] + upper_bounds = parameter_bounds[:, 1] + assert np.all( + (default_value > lower_bounds) & (default_value < upper_bounds) + ), f"Default parameter value of tile {tile_class.__name__} is not within bounds" + @mark.parametrize("tile_class", all_tile_classes) def test_tile_bounds(tile_class): @@ -154,9 +186,10 @@ def test_tile_closure(tile_class): tile_patches, sensitivities = tile_creator.create_tile( closure=closure_direction ) - assert ( - sensitivities is None - ), "Ensure sensitivities for closure are None if no sensitivities are asked" + assert sensitivities is None, ( + f"Expected sensitivities to be None for closure {closure_direction} " + + f"when no sensitivities are requested, got {sensitivities}" + ) check_control_points(tile_patches) @@ -205,12 +238,15 @@ def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): if not tile_creator._sensitivities_implemented: return - # Choose random parameter(s) within bounds - parameter_bounds = np.array(tile_creator._parameter_bounds) - parameters = parameter_bounds[:, 0] + np_rng.random( - len(parameter_bounds) - ) * np.ptp(parameter_bounds, axis=1) - parameters = parameters.reshape(tile_creator._parameters_shape) + def generate_random_parameters(tile_creator, np_rng): + """Generate random parameters within bounds""" + parameter_bounds = np.array(tile_creator._parameter_bounds) + parameters = parameter_bounds[:, 0] + np_rng.random( + len(parameter_bounds) + ) * np.ptp(parameter_bounds, axis=1) + return parameters.reshape(tile_creator._parameters_shape) + + parameters = generate_random_parameters(tile_creator, np_rng) # Test no closure as well as ... closure_directions = [None] From 7e17e1892a4009ce47a15986ac3bbeabca866ead Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 17 Jan 2025 18:03:32 +0100 Subject: [PATCH 108/171] Change input to custom parameter check --- splinepy/microstructure/tiles/tile_base.py | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 5529a286b..08c7be9fe 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -6,6 +6,28 @@ class TileBase(_SplinepyBase): """ Base class for tile objects + + Attributes + --------------- + _dim: int + Dimension in physical space + _para_dim: int + Dimension in parametric space + _evaluation_points: np.ndarray (2D) + Points in parametric space where tile parameters are evaluated. Each parameter + is attributed one evaluation point. This is used for the parametrization spline. + _n_info_per_eval_point: int + Number of tile parameters per evaluation point + _sensitivities_implemented: bool + Whether sensitivities w.r.t. tile parameters are implemented + _closure_directions: list + List of directions in which the closure has been implemented + _parameter_bounds: list> + List of bounds for the tile parameters + _parameters_shape: tuple + Shape of parameters array + _default_parameter_value: float/np.ndarray + Default values for all tile parameters """ _dim = None @@ -93,7 +115,7 @@ def para_dim(self): return self._raise_if_not_set_else_return("_para_dim") @property - def sensitivites_implemented(cls): + def sensitivities_implemented(cls): """Returns whether sensitivities are implemented for the microtile Parameters @@ -305,7 +327,7 @@ def _process_input(self, parameters, parameter_sensitivities): return parameters, n_derivatives, derivatives - def _check_custom_parameter(self, value, param_name, min_bound, max_bound): + def _check_custom_parameter(self, value, param_name, bounds): """Check if a custom tile parameter (e.g. contact length) is within bounds Parameters @@ -314,11 +336,15 @@ def _check_custom_parameter(self, value, param_name, min_bound, max_bound): Value of the custom parameter param_name: str Name of custom parameter - min_bound: float - Lower bound for parameter - max_bound: float - Upper bound for parameter + bounds: list + List of min. and max. bound """ + assert isinstance(bounds, list), "Bounds has to be a list" + assert ( + len(bounds) == 2 + ), "Bounds must consist of a min. and a max. value" + min_bound, max_bound = bounds + if not isinstance(value, float): raise ValueError(f"Invalid type for {param_name}") From cf24f7f29a197ffb5846df5f0ddb9ed8a00ec0b6 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 17 Jan 2025 18:05:17 +0100 Subject: [PATCH 109/171] Add bounds for custom tile parameters to class attributes --- splinepy/microstructure/tiles/armadillo.py | 6 ++++-- splinepy/microstructure/tiles/cross_2d.py | 12 ++++++++---- splinepy/microstructure/tiles/cross_3d.py | 10 +++++++--- splinepy/microstructure/tiles/cross_3d_linear.py | 10 +++++++--- splinepy/microstructure/tiles/double_lattice.py | 4 +++- splinepy/microstructure/tiles/hollow_octagon.py | 4 +++- .../microstructure/tiles/hollow_octagon_extrude.py | 4 +++- splinepy/microstructure/tiles/inverse_cross_3d.py | 10 +++++++--- splinepy/microstructure/tiles/snappy.py | 6 +++--- 9 files changed, 45 insertions(+), 21 deletions(-) diff --git a/splinepy/microstructure/tiles/armadillo.py b/splinepy/microstructure/tiles/armadillo.py index bbac762a9..8e4712d99 100644 --- a/splinepy/microstructure/tiles/armadillo.py +++ b/splinepy/microstructure/tiles/armadillo.py @@ -33,6 +33,8 @@ class Armadillo(_TileBase): _parameters_shape = (1, 1) _default_parameter_value = 0.2 + _CONTACT_LENGTH_BOUNDS = [0.0, 0.99] + def _closing_tile( self, parameters=None, @@ -71,7 +73,7 @@ def _closing_tile( raise ValueError("No closing direction given") self._check_custom_parameter( - contact_length, "contact length", 0.0, 0.99 + contact_length, "contact length", self._CONTACT_LENGTH_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( @@ -5043,7 +5045,7 @@ def create_tile( """ self._check_custom_parameter( - contact_length, "contact length", 0.0, 0.99 + contact_length, "contact length", self._CONTACT_LENGTH_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/cross_2d.py b/splinepy/microstructure/tiles/cross_2d.py index b919ff0ad..3267ce9bc 100644 --- a/splinepy/microstructure/tiles/cross_2d.py +++ b/splinepy/microstructure/tiles/cross_2d.py @@ -34,6 +34,10 @@ class Cross2D(_TileBase): _parameters_shape = (4, 1) _default_parameter_value = 0.2 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] + _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] + _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] + def _closing_tile( self, parameters=None, @@ -79,10 +83,10 @@ def _closing_tile( ) self._check_custom_parameter( - boundary_width, "boundary width", 0.0, 0.5 + boundary_width, "boundary width", self._BOUNDARY_WIDTH_BOUNDS ) self._check_custom_parameter( - filling_height, "filling height", 0.0, 1.0 + filling_height, "filling height", self._FILLING_HEIGHT_BOUNDS ) splines = [] @@ -403,7 +407,7 @@ def create_tile( """ self._check_custom_parameter( - center_expansion, "center expansion", 0.5, 1.5 + center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( @@ -412,7 +416,7 @@ def create_tile( ) max_radius = min(0.5, (0.5 / center_expansion)) - if not _np.all(parameters < max_radius): + if not (_np.all(parameters > 0) and _np.all(parameters < max_radius)): raise ValueError(f"Thickness out of range (0, {max_radius})") # Closure requested, pass to function diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index 5552d998e..0b9955c52 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -36,6 +36,10 @@ class Cross3D(_TileBase): _parameters_shape = (6, 1) _default_parameter_value = 0.2 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] + _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] + _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] + def _closing_tile( self, parameters=None, @@ -81,10 +85,10 @@ def _closing_tile( ) self._check_custom_parameter( - boundary_width, "boundary width", 0.0, 0.5 + boundary_width, "boundary width", self._BOUNDARY_WIDTH_BOUNDS ) self._check_custom_parameter( - filling_height, "filling height", 0.0, 1.0 + filling_height, "filling height", self._FILLING_HEIGHT_BOUNDS ) splines = [] @@ -511,7 +515,7 @@ def create_tile( """ self._check_custom_parameter( - center_expansion, "center expansion", 0.5, 1.5 + center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index 103fa6372..b0aa71bee 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -36,6 +36,10 @@ class Cross3DLinear(_TileBase): _parameters_shape = (6, 1) _default_parameter_value = 0.2 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] + _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] + _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] + def _closing_tile( self, parameters=None, @@ -81,10 +85,10 @@ def _closing_tile( ) self._check_custom_parameter( - boundary_width, "boundary width", 0.0, 0.5 + boundary_width, "boundary width", self._BOUNDARY_WIDTH_BOUNDS ) self._check_custom_parameter( - filling_height, "filling height", 0.0, 1.0 + filling_height, "filling height", self._FILLING_HEIGHT_BOUNDS ) splines = [] @@ -467,7 +471,7 @@ def create_tile( """ self._check_custom_parameter( - center_expansion, "center expansion", 0.5, 1.5 + center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/double_lattice.py b/splinepy/microstructure/tiles/double_lattice.py index d1fb1438a..eda6cd5d8 100644 --- a/splinepy/microstructure/tiles/double_lattice.py +++ b/splinepy/microstructure/tiles/double_lattice.py @@ -25,6 +25,8 @@ class DoubleLattice(_TileBase): _parameters_shape = (1, 2) _default_parameter_value = 0.1 + _CONTACT_LENGTH_BOUNDS = [0.0, 1.0] + def create_tile( self, parameters=None, @@ -55,7 +57,7 @@ def create_tile( microtile_list : list(splines) """ self._check_custom_parameter( - contact_length, "contact length", 0.0, 1.0 + contact_length, "contact length", self._CONTACT_LENGTH_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index c1e3271ec..c099e65c0 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -24,6 +24,8 @@ class HollowOctagon(_TileBase): _parameters_shape = (1, 1) _default_parameter_value = 0.2 + _CONTACT_LENGTH_BOUNDS = [0.0, 0.99] + def _closing_tile( self, parameters=None, @@ -433,7 +435,7 @@ def create_tile( derivatives: list> / None """ self._check_custom_parameter( - contact_length, "contact length", 0.0, 0.99 + contact_length, "contact length", self._CONTACT_LENGTH_BOUNDS ) parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index 6bcfd4313..4d63972ac 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -24,6 +24,8 @@ class HollowOctagonExtrude(_TileBase): _parameters_shape = (1, 1) _default_parameter_value = 0.2 + _CONTACT_LENGTH_BOUNDS = [0.0, 0.99] + def create_tile( self, parameters=None, @@ -57,7 +59,7 @@ def create_tile( """ self._check_custom_parameter( - contact_length, "contact length", 0.0, 0.99 + contact_length, "contact length", self._CONTACT_LENGTH_BOUNDS ) # Process input parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/inverse_cross_3d.py b/splinepy/microstructure/tiles/inverse_cross_3d.py index 5a366ee11..43e5ed632 100644 --- a/splinepy/microstructure/tiles/inverse_cross_3d.py +++ b/splinepy/microstructure/tiles/inverse_cross_3d.py @@ -39,6 +39,10 @@ class InverseCross3D(_TileBase): _parameters_shape = (6, 1) _default_parameter_value = 0.21 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] + _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] + _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] + def _closing_tile( self, parameters=None, @@ -81,10 +85,10 @@ def _closing_tile( ) self._check_custom_parameter( - boundary_width, "boundary width", 0.0, 0.5 + boundary_width, "boundary width", self._BOUNDARY_WIDTH_BOUNDS ) self._check_custom_parameter( - filling_height, "filling height", 0.0, 1.0 + filling_height, "filling height", self._FILLING_HEIGHT_BOUNDS ) splines = [] @@ -1051,7 +1055,7 @@ def create_tile( """ self._check_custom_parameter( - center_expansion, "center expansion", 0.5, 1.5 + center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) # Check if all radii are in allowed range diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index d3202aa63..85801926a 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -28,6 +28,8 @@ class Snappy(_TileBase): _parameter_bounds = [] _parameters_shape = () + _CONTACT_LENGTH_BOUNDS = [0.0, 0.49] + def _closing_tile( self, parameters=None, @@ -69,8 +71,6 @@ def _closing_tile( raise ValueError("No closing direction given") # TODO: parameters are not implemented, therefore do not check params - if parameters is not None: - self.check_params(parameters) if parameter_sensitivities is not None: raise NotImplementedError( @@ -354,7 +354,7 @@ def create_tile( raise ValueError("Invalid parameter, must be > 0.") self._check_custom_parameter( - contact_length, "contact length", 0.0, 0.49 + contact_length, "contact length", self._CONTACT_LENGTH_BOUNDS ) # Check horizontal parameters From 2c986ac02d89ee886244f3b2e1fca7aeb9c6bcb6 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 17 Jan 2025 18:06:13 +0100 Subject: [PATCH 110/171] Add comments according to coderabbitai --- tests/test_microstructure.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index e2fccc026..b109ababa 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -12,6 +12,9 @@ EPS = 1e-7 # TODO: the following tiles fail the closure test +# HollowOctagonExtrude has no closure +# InverseCross3D's closure is special: it does not fill the whole unit cube and does not +# fully cover the closing face CLOSURE_FAILS = [ms.tiles.HollowOctagonExtrude, ms.tiles.InverseCross3D] @@ -58,12 +61,14 @@ def max_identifier(points): face_min_area = sum([volume(patch) for patch in min_patches.patches]) face_max_area = sum([volume(patch) for patch in max_patches.patches]) - assert ( - face_min_area > 1.0 - EPS - ), f"The closure of the {closure_direction}_min surface is not complete" - assert ( - face_max_area > 1.0 - EPS - ), f"The closure of the {closure_direction}_max surface is not complete" + assert face_min_area > 1.0 - EPS, ( + f"The closure of the {closure_direction}_min surface is not complete. " + f"Expected area of 1, got {face_min_area}" + ) + assert face_max_area > 1.0 - EPS, ( + f"The closure of the {closure_direction}_max surface is not complete. " + f"Expected area of 1, got{face_max_area}" + ) # Skip tile if it doesn't support closure if tile_class._closure_directions is None: From 70aa104fbc57caa1101a3ab58164750ef01361ae Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Jan 2025 10:39:48 +0100 Subject: [PATCH 111/171] Add dynamical update of parameter bounds --- splinepy/microstructure/tiles/cross_2d.py | 18 +++++++++------ splinepy/microstructure/tiles/cross_3d.py | 22 +++++++++---------- .../microstructure/tiles/cross_3d_linear.py | 22 +++++++++---------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/splinepy/microstructure/tiles/cross_2d.py b/splinepy/microstructure/tiles/cross_2d.py index 3267ce9bc..990db61f2 100644 --- a/splinepy/microstructure/tiles/cross_2d.py +++ b/splinepy/microstructure/tiles/cross_2d.py @@ -28,12 +28,18 @@ class Cross2D(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = True _closure_directions = ["x_min", "x_max", "y_min", "y_max"] - _parameter_bounds = [ - [0.0, 0.5] - ] * 4 # valid for default center_expansion=1.0 _parameters_shape = (4, 1) _default_parameter_value = 0.2 + # Default value of center_expansion + _center_expansion = 1.0 + + # Dynamical computation of parameter bounds depending on center expansion + @property + def _parameter_bounds(self): + max_radius = min(0.5, (0.5 / self._center_expansion)) + return [[0.0, max_radius]] * 4 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] @@ -410,15 +416,13 @@ def create_tile( center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) + self._center_expansion = center_expansion + parameters, n_derivatives, derivatives = self._process_input( parameters=parameters, parameter_sensitivities=parameter_sensitivities, ) - max_radius = min(0.5, (0.5 / center_expansion)) - if not (_np.all(parameters > 0) and _np.all(parameters < max_radius)): - raise ValueError(f"Thickness out of range (0, {max_radius})") - # Closure requested, pass to function if closure is not None: return self._closing_tile( diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index 0b9955c52..c0edd34ee 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -30,12 +30,18 @@ class Cross3D(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = True _closure_directions = ["z_min", "z_max"] - _parameter_bounds = [ - [0.0, 0.5] - ] * 6 # valid for default center_expansion=1.0 _parameters_shape = (6, 1) _default_parameter_value = 0.2 + # Default value of center_expansion + _center_expansion = 1.0 + + # Dynamical computation of parameter bounds depending on center expansion + @property + def _parameter_bounds(self): + max_radius = min(0.5, (0.5 / self._center_expansion)) + return [[0.0, max_radius]] * 6 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] @@ -518,19 +524,13 @@ def create_tile( center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) + self._center_expansion = center_expansion + parameters, n_derivatives, derivatives = self._process_input( parameters=parameters, parameter_sensitivities=parameter_sensitivities, ) - # Max radius, so there is no tanglement in the crosstile - max_radius = min(0.5, (0.5 / center_expansion)) - if _np.any(parameters > max_radius): - raise ValueError( - f"Radii must be in (0,{max_radius}) for " - f"center_expansion {center_expansion}" - ) - if closure is not None: if closure not in self._closure_directions: raise NotImplementedError( diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index b0aa71bee..c41697857 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -30,12 +30,18 @@ class Cross3DLinear(_TileBase): _n_info_per_eval_point = 1 _sensitivities_implemented = True _closure_directions = ["z_min", "z_max"] - _parameter_bounds = [ - [0.0, 0.5] - ] * 6 # valid for default center_expansion=1.0 _parameters_shape = (6, 1) _default_parameter_value = 0.2 + # Default value of center_expansion + _center_expansion = 1.0 + + # Dynamical computation of parameter bounds depending on center expansion + @property + def _parameter_bounds(self): + max_radius = min(0.5, (0.5 / self._center_expansion)) + return [[0.0, max_radius]] * 6 + _BOUNDARY_WIDTH_BOUNDS = [0.0, 0.5] _FILLING_HEIGHT_BOUNDS = [0.0, 1.0] _CENTER_EXPANSION_BOUNDS = [0.5, 1.5] @@ -474,19 +480,13 @@ def create_tile( center_expansion, "center expansion", self._CENTER_EXPANSION_BOUNDS ) + self._center_expansion = center_expansion + parameters, n_derivatives, derivatives = self._process_input( parameters=parameters, parameter_sensitivities=parameter_sensitivities, ) - # Max radius, so there is no tanglement in the crosstile - max_radius = min(0.5, (0.5 / center_expansion)) - if _np.any(parameters > max_radius): - raise ValueError( - f"Radii must be in (0,{max_radius}) for " - f"center_expansion {center_expansion}" - ) - if closure is not None: return self._closing_tile( parameters=parameters, From d670222844b9e9d81fa890528ca4115c0fab8d0f Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Jan 2025 13:54:48 +0100 Subject: [PATCH 112/171] Test tile class now works with instances so that _parameter_bounds can be either class attribute or instance property --- tests/test_microtiles.py | 52 ++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 4355812d3..97ec2cc6f 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -57,6 +57,10 @@ def test_tile_class(tile_class): tile_class: tile class in splinepy.microstructure.tiles Microtile """ + # Create instance of class + tile_instance = tile_class() + tile_name = tile_class.__name__ + required_class_variables = { "_para_dim": int, "_dim": int, @@ -68,69 +72,75 @@ def test_tile_class(tile_class): } # Get tile class' objects - members = [attr for attr in dir(tile_class) if not attr.startswith("__")] + members = [ + attr for attr in dir(tile_instance) if not attr.startswith("__") + ] # Class must have function create_tile() assert hasattr( - tile_class, "create_tile" - ), f"Tile class {tile_class.__name__} must have create_tile() method" + tile_instance, "create_tile" + ), f"Tile class {tile_name} must have create_tile() method" + # Tile must be able to take parameters and sensitivities as input - create_parameters = getfullargspec(tile_class.create_tile).args + create_parameters = getfullargspec(tile_instance.create_tile).args for required_param in ["parameters", "parameter_sensitivities"]: assert required_param in create_parameters, ( - f"{tile_class.__name__}.create_tile() must have '{required_param}' as an " + f"{tile_name}.create_tile() must have '{required_param}' as an " "input parameter" ) # Ensure closure can be handled correctly if "closure" in create_parameters: assert "_closure_directions" in members, ( - "Tile class has closure ability. The available closure directions " - + "are missing" + f"Tile class {tile_name} has closure ability. The available closure " + + "directions are missing" ) assert hasattr( - tile_class, "_closing_tile" - ), "Tile class has closure ability but no _closing_tile() function!" + tile_instance, "_closing_tile" + ), f"Tile class {tile_name} has closure ability but no _closing_tile() function" # Check if tile class has all required variables and they are the correct type for required_variable, var_type in required_class_variables.items(): assert ( required_variable in members - ), f"Tile class needs to have member variable '{required_variable}'" + ), f"Tile class {tile_name} needs to have member variable '{required_variable}'" assert isinstance( - eval(f"tile_class.{required_variable}"), var_type - ), f"Variable {required_variable} needs to be of type {var_type}" + eval(f"tile_instance.{required_variable}"), var_type + ), ( + f"Variable {required_variable} needs to be of type {var_type} and not" + f"{required_variable}" + ) # Check default parameter value if there is one - if tile_class._parameters_shape != (): + if tile_instance._parameters_shape != (): # Assert that there is a default value - assert hasattr(tile_class, "_default_parameter_value"), ( - f'{tile_class.__name__} must have "_default_parameter_value" as a class ' + assert hasattr(tile_instance, "_default_parameter_value"), ( + f'{tile_name} must have "_default_parameter_value" as a class ' " attribute." ) # Check the default value's type - default_value = tile_class._default_parameter_value + default_value = tile_instance._default_parameter_value if isinstance(default_value, np.ndarray): # Check the dimensions - assert default_value.shape == tile_class._parameters_shape, ( - f"Default parameter values for tile {tile_class.__name__} has the wrong" + assert default_value.shape == tile_instance._parameters_shape, ( + f"Default parameter values for tile {tile_name} has the wrong" " dimensions" ) # Check if default values are within bounds default_value = default_value.ravel() elif not isinstance(default_value, float): raise ValueError( - f"Default parameter value for tile {tile_class.__name__} must either be" + f"Default parameter value for tile {tile_name} must either be" "a float or a numpy array" ) # Check if default values are within bounds - parameter_bounds = np.asarray(tile_class._parameter_bounds) + parameter_bounds = np.asarray(tile_instance._parameter_bounds) lower_bounds = parameter_bounds[:, 0] upper_bounds = parameter_bounds[:, 1] assert np.all( (default_value > lower_bounds) & (default_value < upper_bounds) - ), f"Default parameter value of tile {tile_class.__name__} is not within bounds" + ), f"Default parameter value of tile {tile_name} is not within bounds" @mark.parametrize("tile_class", all_tile_classes) From 30bda307ec2b275fbc6416da7eedd1ce02de3469 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Jan 2025 14:11:23 +0100 Subject: [PATCH 113/171] Define class properties as class attribute or instance attribute --- splinepy/microstructure/tiles/tile_base.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 08c7be9fe..28ed86d55 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -129,7 +129,7 @@ def sensitivities_implemented(cls): return cls._raise_if_not_set_else_return("_sensitivities_implemented") @property - def closure_directions(self): + def closure_directions(cls): """Returns the available closure directions of the microtile Parameters @@ -140,11 +140,14 @@ def closure_directions(self): ------- directions: None/list """ - return self._closure_directions + return cls._closure_directions @property - def parameter_bounds(cls): - """Returns the bounds for the microtiles' parameters + def parameter_bounds(self): + """Returns the bounds for the microtiles' parameters. + + Depending on the tile, parameter bounds can change (e.g. Cross2D). Therefore, it + is instance-dependent and self instead of cls is used. Parameters ---------- @@ -154,7 +157,7 @@ def parameter_bounds(cls): ------- bounds: list> """ - return cls._raise_if_not_set_else_return("_parameter_bounds") + return self._raise_if_not_set_else_return("_parameter_bounds") @property def parameters_shape(cls): From fcbb133f49b178c5d6baaa118f891863e1d48f85 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Jan 2025 15:01:46 +0100 Subject: [PATCH 114/171] Accept integers as tile parameters as well --- splinepy/microstructure/tiles/tile_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 28ed86d55..bf750b844 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -348,7 +348,7 @@ def _check_custom_parameter(self, value, param_name, bounds): ), "Bounds must consist of a min. and a max. value" min_bound, max_bound = bounds - if not isinstance(value, float): + if not isinstance(value, (int, float)): raise ValueError(f"Invalid type for {param_name}") if not ((value > min_bound) and (value < max_bound)): From 0cd340ecb33e5c82f2997e852ae7e3fa4ba1e885 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Jan 2025 15:03:33 +0100 Subject: [PATCH 115/171] Add pytest skip for unfinished/hard-to-test tiles and some coderabbitai suggestions --- tests/test_microstructure.py | 35 ++++++++++++++-------- tests/test_microtiles.py | 58 +++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index b109ababa..9a0a0f0a7 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -1,11 +1,17 @@ import numpy as np -from pytest import mark +from pytest import mark, skip import splinepy.microstructure as ms from splinepy.helpme.create import box from splinepy.helpme.integrate import volume all_tile_classes = list(ms.tiles.everything().values()) +# Tile classes where closure should be tested +tile_classes_with_closure = [ + tile_class + for tile_class in all_tile_classes + if tile_class._closure_directions is not None +] TILING = [2, 2, 2] BOX_DIMENSIONS = [1, 1, 1] @@ -18,7 +24,7 @@ CLOSURE_FAILS = [ms.tiles.HollowOctagonExtrude, ms.tiles.InverseCross3D] -@mark.parametrize("tile_class", all_tile_classes) +@mark.parametrize("tile_class", tile_classes_with_closure) def test_closing_face(tile_class): """Check if closing face is working @@ -28,6 +34,19 @@ def test_closing_face(tile_class): Microtile """ + # Skip tile if it doesn't support closure + if tile_class._closure_directions is None: + skip( + f"Tile {tile_class.__name__} does not have a closure implementation. Skip" + ) + + # TODO: right now skip tiles which have faulty closures + if tile_class in CLOSURE_FAILS: + skip( + f"Known issue: skip closure test for {tile_class.__name__}, which may have " + "closure implementation" + ) + def check_if_closed(multipatch, closure_direction): """Helper function to see if multipatch has a closing surface @@ -67,17 +86,9 @@ def max_identifier(points): ) assert face_max_area > 1.0 - EPS, ( f"The closure of the {closure_direction}_max surface is not complete. " - f"Expected area of 1, got{face_max_area}" + f"Expected area of 1, instead got {face_max_area}" ) - # Skip tile if it doesn't support closure - if tile_class._closure_directions is None: - return - - # TODO: right now skip tiles which have faulty closures - if tile_class in CLOSURE_FAILS: - return - tile_creator = tile_class() generator = ms.microstructure.Microstructure( deformation_function=box(*BOX_DIMENSIONS[: tile_creator._dim]), @@ -108,7 +119,7 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): heps: float Perturbation size for finite difference evaluation. Defined in conftest.py n_test_points: int - Number of testing points int the parametric domain. Defined in conftest.py + Number of testing points in the parametric domain. Defined in conftest.py """ tile_creator = tile_class() diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 97ec2cc6f..4423783e7 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -1,7 +1,7 @@ from inspect import getfullargspec import numpy as np -from pytest import mark +from pytest import mark, skip import splinepy.microstructure as ms from splinepy.utils.data import cartesian_product as _cartesian_product @@ -9,6 +9,18 @@ EPS = 1e-8 all_tile_classes = list(ms.tiles.everything().values()) +# Tile classes where closure should be tested +tile_classes_with_closure = [ + tile_class + for tile_class in all_tile_classes + if tile_class._closure_directions is not None +] +# Tile classes where sensitivities should be tested +tile_classes_with_sensitivities = [ + tile_class + for tile_class in all_tile_classes + if tile_class._sensitivities_implemented +] # Skip certain tile classes for parameters testing skip_tiles = [ @@ -105,7 +117,7 @@ def test_tile_class(tile_class): required_variable in members ), f"Tile class {tile_name} needs to have member variable '{required_variable}'" assert isinstance( - eval(f"tile_instance.{required_variable}"), var_type + getattr(tile_instance, required_variable), var_type ), ( f"Variable {required_variable} needs to be of type {var_type} and not" f"{required_variable}" @@ -130,7 +142,7 @@ def test_tile_class(tile_class): default_value = default_value.ravel() elif not isinstance(default_value, float): raise ValueError( - f"Default parameter value for tile {tile_name} must either be" + f"Default parameter value for tile {tile_name} must either be " "a float or a numpy array" ) @@ -160,7 +172,10 @@ def test_tile_bounds(tile_class): # Skip certain classes for testing if tile_class in skip_tiles: - return + skip( + "Known issue: skip bound test for non-default parameter values for tile " + f"{tile_class.__name__}" + ) # Go through all extremes of parameters and ensure that also they create # tiles within unit square/cube @@ -177,7 +192,7 @@ def test_tile_bounds(tile_class): check_control_points(tile_patches) -@mark.parametrize("tile_class", all_tile_classes) +@mark.parametrize("tile_class", tile_classes_with_closure) def test_tile_closure(tile_class): """Check if closing tiles also lie in unit cube. @@ -186,10 +201,11 @@ def test_tile_closure(tile_class): tile_class: tile class in splinepy.microstructure.tiles Microtile """ + tile_name = tile_class.__name__ # Skip tile if if does not support closure if tile_class._closure_directions is None: - return + skip(f"Tile {tile_name} does not have a closure implementation. Skip") tile_creator = tile_class() # Go through all implemented closure directions for closure_direction in tile_creator._closure_directions: @@ -206,7 +222,10 @@ def test_tile_closure(tile_class): # Also check non-default parameters # Skip certain classes for testing if tile_class in skip_tiles: - return + skip( + "Known issue: skip closure test for non-default parameter for tile " + f"{tile_name}" + ) # Go through all extremes of parameters and ensure that also they create # tiles within unit square/cube @@ -226,7 +245,7 @@ def test_tile_closure(tile_class): check_control_points(tile_patches) -@mark.parametrize("tile_class", all_tile_classes) +@mark.parametrize("tile_class", tile_classes_with_sensitivities) def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): """Testing the correctness of the tile derivatives using Finite Differences. This includes every closure and no closure, every parameter and every patch @@ -246,7 +265,7 @@ def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): tile_creator = tile_class() # Skip test if tile class has no implemented sensitivities if not tile_creator._sensitivities_implemented: - return + skip(f"Tile {tile_class.__name__} has no sensitivities implemented") def generate_random_parameters(tile_creator, np_rng): """Generate random parameters within bounds""" @@ -315,16 +334,13 @@ def generate_random_parameters(tile_creator, np_rng): for i_patch, deriv_orig, deriv_fd in zip( range(n_patches), deriv_evaluations, fd_sensitivities ): - assert np.allclose(deriv_orig, deriv_fd), ( - "Implemented derivative calculation for tile class" - + f"{tile_class}, with closure {closure}, parameter " - + f"{i_parameter+1}/{n_info_per_eval_point} at patch " - + f"{i_patch+1}/{n_patches} does not match the derivative " - + "obtained using Finite Differences at the following evaluation " - + "points:\n" - + str(eval_points) - + "\nImplemented derivative:\n" - + str(deriv_orig) - + "\nFinite difference derivative:\n" - + str(deriv_fd) + message = ( + f"Implemented derivative calculation for tile class {tile_class} " + f"with closure {closure}, parameter {i_parameter+1}/" + f"{n_info_per_eval_point} at patch {i_patch+1}/{n_patches} does not" + f" match the derivative obtained using Finite Differences at the " + f"following evaluation points:\n {eval_points}\nImplemented " + f"derivative:\n{deriv_orig}\nFinite Difference derivative:\n" + f"{deriv_fd}" ) + assert np.allclose(deriv_orig, deriv_fd), message From 3b5d01a899ab51cd091557fbc92a801897bdba3e Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Jan 2025 15:12:32 +0100 Subject: [PATCH 116/171] Ignore parameters (unused in function) for pre-commit --- splinepy/microstructure/tiles/snappy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index 85801926a..7ae03c671 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -32,7 +32,7 @@ class Snappy(_TileBase): def _closing_tile( self, - parameters=None, + parameters=None, # noqa: ARG002 parameter_sensitivities=None, # TODO closure=None, contact_length=0.1, From 140e21a886eb6b2c635af0fd4cc86dc273c14835 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 13:21:09 +0100 Subject: [PATCH 117/171] Enhance error message for unsupported closure directions --- splinepy/microstructure/tiles/cross_3d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/splinepy/microstructure/tiles/cross_3d.py b/splinepy/microstructure/tiles/cross_3d.py index c0edd34ee..95fe347b0 100644 --- a/splinepy/microstructure/tiles/cross_3d.py +++ b/splinepy/microstructure/tiles/cross_3d.py @@ -534,7 +534,8 @@ def create_tile( if closure is not None: if closure not in self._closure_directions: raise NotImplementedError( - f"Closure '{closure}' not implemented" + f"Closure '{closure}' not implemented. Supported closures are: " + + f"{self._closure_directions}" ) return self._closing_tile( parameters=parameters, From 1ad44799711a612e54c4e8e131fe1164a95b55d5 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 13:24:16 +0100 Subject: [PATCH 118/171] Replace assert statements with proper exception handling --- splinepy/microstructure/tiles/tile_base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index bf750b844..80dbb3292 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -342,10 +342,11 @@ def _check_custom_parameter(self, value, param_name, bounds): bounds: list List of min. and max. bound """ - assert isinstance(bounds, list), "Bounds has to be a list" - assert ( - len(bounds) == 2 - ), "Bounds must consist of a min. and a max. value" + + if not isinstance(bounds, list): + raise TypeError("bounds has to be a list") + if len(bounds) != 2: + raise ValueError("Bounds must consist of a min. and a max. value") min_bound, max_bound = bounds if not isinstance(value, (int, float)): From cc84b5c49da1a830b41da625e5fdc52362cc5b4b Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 15:09:27 +0100 Subject: [PATCH 119/171] Add tests to check if error is validly thrown when parameters are wrong --- tests/conftest.py | 8 +++++ tests/test_microtiles.py | 70 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index ef4b69ff2..a437bc09e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,6 +65,14 @@ def n_test_points(): return 10 +@pytest.fixture +def big_perturbation(): + """Value for perturbation of parameters value + + The number 0.1 is small enough to fit the range of most tile parameters""" + return 0.1 + + # initializing a spline should be a test itself, so provide `dict_spline` # this is "iga-book"'s fig 2.15. @pytest.fixture diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 4423783e7..27de3cc18 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -1,7 +1,7 @@ from inspect import getfullargspec import numpy as np -from pytest import mark, skip +from pytest import mark, raises, skip import splinepy.microstructure as ms from splinepy.utils.data import cartesian_product as _cartesian_product @@ -344,3 +344,71 @@ def generate_random_parameters(tile_creator, np_rng): f"{deriv_fd}" ) assert np.allclose(deriv_orig, deriv_fd), message + + +@mark.parametrize("tile_class", all_tile_classes) +def test_invalid_parameter_values(tile_class, big_perturbation): + """Testing whether the tile class correctly raises an error if invalid parameters + are given. Current tests include too low or too high parameter values and wrong + shapes of the parameter array. + + Parameters + ---------- + tile_class: tile class in splinepy.microstructure.tiles + Microtile class + """ + tile_creator = tile_class() + # For certain tiles skip tests + if len(tile_creator._parameter_bounds) == 0: + skip( + f"Skip check for invalid parameters for tile {tile_class.__name__} " + "since there are no parameter bounds implemented for this tile" + ) + + parameter_bounds = np.asarray(tile_creator._parameter_bounds) + + # Check if tile class correctly raises an error if parameter values are too low + parameters_too_low = ( + parameter_bounds[:, 0].reshape(tile_creator._parameters_shape) + - big_perturbation + ) + with raises(ValueError) as exc_info_low: + tile_creator.create_tile(parameters=parameters_too_low) + # Check if the exception message calls TileBase.check_params() + assert "must be within the following bounds: lower: " in str( + exc_info_low.value + ), ( + f"Tile class {tile_class.__name__} must call TileBase.check_params() and raise", + " a ValueError if parameters values are too low", + ) + + # Check the same if parameter values are too high + parameters_too_high = ( + parameter_bounds[:, 1].reshape(tile_creator._parameters_shape) + + big_perturbation + ) + with raises(ValueError) as exc_info_high: + tile_creator.create_tile(parameters=parameters_too_high) + # Check if the exception message calls TileBase.check_params() + assert "must be within the following bounds: lower: " in str( + exc_info_high.value + ), ( + f"Tile class {tile_class.__name__} must call TileBase.check_params() and raise", + " a ValueError if parameters values are too high", + ) + + # Test if error is correctly thrown if the parameter shape is incompatible + # Take parameters in the middle of the bounds and double the array size + parameters_middle = np.mean(parameter_bounds, axis=1).reshape( + tile_creator._parameters_shape + ) + parameters_middle = np.tile(parameters_middle, [2, 1]) + # Check if the error is correctly raised + with raises(TypeError) as exc_info_size: + tile_creator.create_tile(parameters=parameters_middle) + assert "Mismatch in parameter size, expected" in str( + exc_info_size.value + ), ( + f"Tile class {tile_class.__name__} must call TileBase.check_params() and raise", + " a TypeError if the given parameters have the wrong shape", + ) From 9b94403b622ef10c476b7302ab8bd1cba44733ce Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 15:26:53 +0100 Subject: [PATCH 120/171] Enhance error message clarity --- tests/test_microstructure.py | 17 ++++++++++------- tests/test_microtiles.py | 20 +++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 9a0a0f0a7..4c3e62942 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -13,15 +13,17 @@ if tile_class._closure_directions is not None ] +# Fixed auxiliary variables specific to microstructure testing TILING = [2, 2, 2] BOX_DIMENSIONS = [1, 1, 1] EPS = 1e-7 -# TODO: the following tiles fail the closure test -# HollowOctagonExtrude has no closure -# InverseCross3D's closure is special: it does not fill the whole unit cube and does not -# fully cover the closing face -CLOSURE_FAILS = [ms.tiles.HollowOctagonExtrude, ms.tiles.InverseCross3D] +# TODO(#458): the following tiles fail the closure test +CLOSURE_FAILS = { + ms.tiles.HollowOctagonExtrude: "has no closure implemented", + ms.tiles.InverseCross3D: "closure is special: it does not fill the whole unit " + + "cube and does not fully cover the closing face", +} @mark.parametrize("tile_class", tile_classes_with_closure) @@ -42,9 +44,10 @@ def test_closing_face(tile_class): # TODO: right now skip tiles which have faulty closures if tile_class in CLOSURE_FAILS: + reason = CLOSURE_FAILS[tile_class] skip( - f"Known issue: skip closure test for {tile_class.__name__}, which may have " - "closure implementation" + f"Known issue: skip closure test for {tile_class.__name__}, " + f"reason: {reason}" ) def check_if_closed(multipatch, closure_direction): diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 27de3cc18..a27476abf 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -23,10 +23,10 @@ ] # Skip certain tile classes for parameters testing -skip_tiles = [ - ms.tiles.EllipsVoid, # Control points easily lie outside unitcube - ms.tiles.Snappy, # Has no "parameters" -] +skip_tiles = { + ms.tiles.EllipsVoid: "control points easily lie outside unitcube", + ms.tiles.Snappy: "has no 'parameters' implemented", +} def check_control_points(tile_patches): @@ -35,9 +35,11 @@ def check_control_points(tile_patches): # Go through all patches for tile_patch in tile_patches: cps = tile_patch.control_points - assert np.all( - (cps >= 0.0 - EPS) & (cps <= 1.0 + EPS) - ), "Control points of tile must lie inside the unit square/cube" + assert np.all((cps >= 0.0 - EPS) & (cps <= 1.0 + EPS)), ( + "Control points of tile must lie inside the unit square/cube. " + + "Found points outside bounds: " + f"{cps[~((cps >= 0.0 - EPS) & (cps <= 1.0 + EPS))]}" + ) def make_bounds_feasible(bounds): @@ -174,7 +176,7 @@ def test_tile_bounds(tile_class): if tile_class in skip_tiles: skip( "Known issue: skip bound test for non-default parameter values for tile " - f"{tile_class.__name__}" + f"{tile_class.__name__}. Reason: {skip_tiles[tile_class]}" ) # Go through all extremes of parameters and ensure that also they create @@ -224,7 +226,7 @@ def test_tile_closure(tile_class): if tile_class in skip_tiles: skip( "Known issue: skip closure test for non-default parameter for tile " - f"{tile_name}" + f"{tile_name}. Reason: {skip_tiles[tile_class]}" ) # Go through all extremes of parameters and ensure that also they create From 5f8bac88a16055e05af8de412166c93a7f6a8d0f Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 16:19:37 +0100 Subject: [PATCH 121/171] Add fourth order accurate Finite Difference calculation of parameter sensitvities --- tests/conftest.py | 16 +++++++- tests/test_microtiles.py | 83 ++++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a437bc09e..58722fdf5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,12 +47,12 @@ def heps(): """ Perturbation/step size for finite difference evaluation of derivative/sensitivity. - The value 1e-7 is arbitrary, but is a a compromise between: + The value 1e-4 is arbitrary, but is a a compromise between: - Being small enough to ensure the finite difference calculation being accurate enough - Being large enough to avoid round-off error in floating-point arithmetic """ - return 1e-7 + return 1e-5 @pytest.fixture @@ -73,6 +73,18 @@ def big_perturbation(): return 0.1 +@pytest.fixture +def fd_derivative_stepsizes_and_weights(): + """Stepsizes and weights for the calculation of the derivative using finite + differences. Using fourth-order accurate centered scheme. + + Returns + ------- + stepsizes_and_weights, denominator: dict + """ + return {-2: 1 / 12, -1: -8 / 12, 1: 8 / 12, 2: -1 / 12} + + # initializing a spline should be a test itself, so provide `dict_spline` # this is "iga-book"'s fig 2.15. @pytest.fixture diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index a27476abf..14f839fcf 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -248,7 +248,13 @@ def test_tile_closure(tile_class): @mark.parametrize("tile_class", tile_classes_with_sensitivities) -def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): +def test_tile_derivatives( + tile_class, + np_rng, + heps, + n_test_points, + fd_derivative_stepsizes_and_weights, +): """Testing the correctness of the tile derivatives using Finite Differences. This includes every closure and no closure, every parameter and every patch by evaluating at random points and for random parameters. @@ -263,6 +269,8 @@ def test_tile_derivatives(tile_class, np_rng, heps, n_test_points): Perturbation size for finite difference evaluation. Defined in conftest.py n_test_points: int Number of testing points in the parametric domain. Defined in conftest.py + fd_derivative_stepsizes_and_weights: dict + Stepsizes and weights for finite difference scheme. Defined in conftest.py """ tile_creator = tile_class() # Skip test if tile class has no implemented sensitivities @@ -289,20 +297,63 @@ def generate_random_parameters(tile_creator, np_rng): n_eval_points = tile_creator._evaluation_points.shape[0] n_info_per_eval_point = tile_creator._n_info_per_eval_point + def derivative_finite_difference_evaluation( + tile_creator, parameters, i_parameter, closure, eval_points, n_patches + ): + """Compute the derivative using fourth-order accurate centered finite + differences + + Parameters + ------------ + tile_creator: instance of microtile + Microtile class + parameters: np.ndarray + Tile parameter values + i_parameter: int + Index of parameter, on which the FD derivative calculation should be + performed on + closure: None/str + Closure direction of tile + eval_points: np.ndarray + Evaluation points on where to evaluate the derivative on + n_patches: int + Number of patches of tile + """ + # Initialize array for FD evaluation + fd_sensitivities = np.zeros( + (n_patches, len(eval_points), tile_creator.dim) + ) + + # Go through the + for stepsize, weighting in fd_derivative_stepsizes_and_weights.items(): + # Perturb parameter with respective stepsize + parameters_perturbed = parameters.copy() + parameters_perturbed[:, i_parameter] += stepsize * heps + # Create patches with perturbed parameter value + splines_perturbed, _ = tile_creator.create_tile( + parameters=parameters_perturbed, closure=closure + ) + fd_sensitivities += np.array( + [ + weighting / heps * spl.evaluate(eval_points) + for spl in splines_perturbed + ] + ) + + return fd_sensitivities + # Test each closure direction for closure in closure_directions: # Evaluate tile with given parameter and closure configuration splines_orig, _ = tile_creator.create_tile( parameters=parameters, closure=closure ) + n_patches = len(splines_orig) # Set evaluation points as random spots in the parametric space eval_points = np_rng.random((n_test_points, splines_orig[0].para_dim)) - splines_orig_evaluations = [ - spl.evaluate(eval_points) for spl in splines_orig - ] # Go through all the parameters individually for i_parameter in range(n_info_per_eval_point): - # Get derivatives w.r.t. one parameter + # Get implemented derivatives w.r.t. one parameter parameter_sensitivities = np.zeros( (n_eval_points, n_info_per_eval_point, 1) ) @@ -316,23 +367,15 @@ def generate_random_parameters(tile_creator, np_rng): deriv.evaluate(eval_points) for deriv in derivatives[0] ] # Perform finite difference evaluation - parameters_perturbed = parameters.copy() - parameters_perturbed[:, i_parameter] += heps - splines_perturbed, _ = tile_creator.create_tile( - parameters=parameters_perturbed, closure=closure + fd_sensitivities = derivative_finite_difference_evaluation( + tile_creator, + parameters, + i_parameter, + closure, + eval_points, + n_patches, ) - spline_perturbed_evaluations = [ - spl.evaluate(eval_points) for spl in splines_perturbed - ] - # Evaluate finite difference gradient - fd_sensitivities = [ - (spl_pert - spl_orig) / heps - for spl_pert, spl_orig in zip( - spline_perturbed_evaluations, splines_orig_evaluations - ) - ] # Check every patch - n_patches = len(fd_sensitivities) for i_patch, deriv_orig, deriv_fd in zip( range(n_patches), deriv_evaluations, fd_sensitivities ): From 7c57106eccc465bfdb7f9d4adbfd54280d1c222e Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 16:44:06 +0100 Subject: [PATCH 122/171] Change some error types --- splinepy/microstructure/tiles/snappy.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index 7ae03c671..d46cd4da8 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -28,7 +28,7 @@ class Snappy(_TileBase): _parameter_bounds = [] _parameters_shape = () - _CONTACT_LENGTH_BOUNDS = [0.0, 0.49] + _CONTACT_LENGTH_BOUNDS = [0.0, 0.5] def _closing_tile( self, @@ -70,8 +70,6 @@ def _closing_tile( if closure is None: raise ValueError("No closing direction given") - # TODO: parameters are not implemented, therefore do not check params - if parameter_sensitivities is not None: raise NotImplementedError( "Derivatives are not implemented for this tile yet" @@ -295,7 +293,7 @@ def _closing_tile( _Bezier(degrees=[3, 1], control_points=spline_5) ) else: - raise ValueError( + raise NotImplementedError( "Closing tile is only implemented for y-enclosure" ) @@ -349,7 +347,7 @@ def create_tile( for param in [a, b, c, r, contact_length]: if not isinstance(param, float): - raise ValueError(f"Invalid Type, {param} is not float") + raise TypeError(f"Invalid Type, {param} is not float") if param < 0: raise ValueError("Invalid parameter, must be > 0.") From 3103a643965d50a6c6703e487468b7ab43fa6c82 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 6 Feb 2025 17:14:14 +0100 Subject: [PATCH 123/171] Add clarification of parameter bounds --- splinepy/microstructure/tiles/tile_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 80dbb3292..83d6657f3 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -146,8 +146,8 @@ def closure_directions(cls): def parameter_bounds(self): """Returns the bounds for the microtiles' parameters. - Depending on the tile, parameter bounds can change (e.g. Cross2D). Therefore, it - is instance-dependent and self instead of cls is used. + Depending on the tile, parameter bounds can dynamically change (e.g. Cross2D). + Therefore, it is instance-dependent and self instead of cls is used. Parameters ---------- From dd4f0c00b393919d7147f68d99439ba42d0a6bb3 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 7 Feb 2025 12:21:33 +0100 Subject: [PATCH 124/171] Add some suggestions from coderabbit --- splinepy/microstructure/tiles/snappy.py | 6 ++++-- splinepy/microstructure/tiles/tile_base.py | 7 +++++-- tests/conftest.py | 3 +-- tests/test_microtiles.py | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/splinepy/microstructure/tiles/snappy.py b/splinepy/microstructure/tiles/snappy.py index d46cd4da8..24a910e0d 100644 --- a/splinepy/microstructure/tiles/snappy.py +++ b/splinepy/microstructure/tiles/snappy.py @@ -346,8 +346,10 @@ def create_tile( """ for param in [a, b, c, r, contact_length]: - if not isinstance(param, float): - raise TypeError(f"Invalid Type, {param} is not float") + if not isinstance(param, (int, float)): + raise TypeError( + f"Invalid Type, {param} is neither int nor float" + ) if param < 0: raise ValueError("Invalid parameter, must be > 0.") diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 83d6657f3..10a0ee595 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -224,9 +224,12 @@ def check_params(self, parameters): parameters.ravel() < upper_bounds ) if not _np.all(within_bounds): + out_of_bounds = parameters[ + ~within_bounds.reshape(parameters.shape) + ] raise ValueError( - f"The parameters {parameters} must be within the following bounds: " - + f"lower: {lower_bounds} and upper: {upper_bounds}" + f"The following parameters are out of bounds: {out_of_bounds}. ", + f"Expected bounds: lower: {lower_bounds} and upper: {upper_bounds}", ) return True diff --git a/tests/conftest.py b/tests/conftest.py index 58722fdf5..87121d236 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,9 +47,8 @@ def heps(): """ Perturbation/step size for finite difference evaluation of derivative/sensitivity. - The value 1e-4 is arbitrary, but is a a compromise between: + The value 1e-5 is arbitrary, but is a a compromise between: - Being small enough to ensure the finite difference calculation being accurate - enough - Being large enough to avoid round-off error in floating-point arithmetic """ return 1e-5 diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 14f839fcf..126f58ecf 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -420,7 +420,7 @@ def test_invalid_parameter_values(tile_class, big_perturbation): with raises(ValueError) as exc_info_low: tile_creator.create_tile(parameters=parameters_too_low) # Check if the exception message calls TileBase.check_params() - assert "must be within the following bounds: lower: " in str( + assert "The following parameters are out of bounds: " in str( exc_info_low.value ), ( f"Tile class {tile_class.__name__} must call TileBase.check_params() and raise", @@ -435,7 +435,7 @@ def test_invalid_parameter_values(tile_class, big_perturbation): with raises(ValueError) as exc_info_high: tile_creator.create_tile(parameters=parameters_too_high) # Check if the exception message calls TileBase.check_params() - assert "must be within the following bounds: lower: " in str( + assert "The following parameters are out of bounds: " in str( exc_info_high.value ), ( f"Tile class {tile_class.__name__} must call TileBase.check_params() and raise", From 05830d8245ac221f0bc85343434cd0c15cd67f54 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 7 Feb 2025 13:55:07 +0100 Subject: [PATCH 125/171] Add more suggestions from coderabbit --- tests/conftest.py | 9 ++++++--- tests/test_microtiles.py | 13 +++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 87121d236..2b866da69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,7 +47,7 @@ def heps(): """ Perturbation/step size for finite difference evaluation of derivative/sensitivity. - The value 1e-5 is arbitrary, but is a a compromise between: + The value 1e-5 is arbitrary, but is a compromise between: - Being small enough to ensure the finite difference calculation being accurate - Being large enough to avoid round-off error in floating-point arithmetic """ @@ -59,7 +59,8 @@ def n_test_points(): """ Number of random testing points (in parametric domain) - The number 10 is arbitrary and should ensure to have good test coverage + The number 10 is arbitrary and should ensure to have good test coverage. Increasing + this number could yield more thorough tests at the cost of longer runtime. """ return 10 @@ -68,7 +69,9 @@ def n_test_points(): def big_perturbation(): """Value for perturbation of parameters value - The number 0.1 is small enough to fit the range of most tile parameters""" + The number 0.1 is chosen arbitrarily. This value is for testing out of bounds + parameter values. It is designed to add to the maximum or subtract from the + minimum bound to delibarately make the values out of the bounds.""" return 0.1 diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 126f58ecf..48ae1e508 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -6,6 +6,7 @@ import splinepy.microstructure as ms from splinepy.utils.data import cartesian_product as _cartesian_product +# Tolerance value for checking control points EPS = 1e-8 all_tile_classes = list(ms.tiles.everything().values()) @@ -31,14 +32,14 @@ def check_control_points(tile_patches): """Helper function. Check if all of tile's control points all lie within unit - square/cube""" + square/cube. The tolerance is defined by EPS""" # Go through all patches for tile_patch in tile_patches: cps = tile_patch.control_points - assert np.all((cps >= 0.0 - EPS) & (cps <= 1.0 + EPS)), ( + valid_cp_indices = (cps >= 0.0 - EPS) & (cps <= 1.0 + EPS) + assert np.all(valid_cp_indices), ( "Control points of tile must lie inside the unit square/cube. " - + "Found points outside bounds: " - f"{cps[~((cps >= 0.0 - EPS) & (cps <= 1.0 + EPS))]}" + + f"Found points outside bounds: {cps[~(valid_cp_indices)]}" ) @@ -121,8 +122,8 @@ def test_tile_class(tile_class): assert isinstance( getattr(tile_instance, required_variable), var_type ), ( - f"Variable {required_variable} needs to be of type {var_type} and not" - f"{required_variable}" + f"Variable {required_variable} must be of type {var_type}, but found type " + f"{type(getattr(tile_instance, required_variable))}" ) # Check default parameter value if there is one From 936036c3763f8172cf1accd1a1e3b9bfff0571fe Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Mon, 2 Jun 2025 11:58:41 +0200 Subject: [PATCH 126/171] Nit: Rename symbol to pass pre-commit --- tests/conftest.py | 2 +- tests/test_microstructure.py | 10 +++++----- tests/test_microtiles.py | 27 +++++++++++++-------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2b866da69..b5fa224ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,7 +43,7 @@ def queries_3D(): # hard-coded values to keep the same for derivative/sensitivity calculations @pytest.fixture -def heps(): +def h_eps(): """ Perturbation/step size for finite difference evaluation of derivative/sensitivity. diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 4c3e62942..2ca2c1da9 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -108,7 +108,7 @@ def max_identifier(points): @mark.parametrize("tile_class", all_tile_classes) -def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): +def test_macro_sensitivities(tile_class, np_rng, h_eps, n_test_points): """Testing the correctness of the derivatives of the whole microstructure w.r.t. the deformation function's control points. It is tested by evaluating the derivative obtained via finite differences. The values are evaluated at random points. @@ -119,7 +119,7 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): Microtile np_rng: numpy.random._generator.Generator Default random number generator - heps: float + h_eps: float Perturbation size for finite difference evaluation. Defined in conftest.py n_test_points: int Number of testing points in the parametric domain. Defined in conftest.py @@ -147,7 +147,7 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): for ii_ctps in range(n_cps): # Gradient through finite differences deformation_function_perturbed = deformation_function_orig.copy() - deformation_function_perturbed.cps[ii_ctps, :] += heps + deformation_function_perturbed.cps[ii_ctps, :] += h_eps generator.deformation_function = deformation_function_perturbed multipatch_perturbed = generator.create() microstructure_perturbed_evaluations = [ @@ -156,7 +156,7 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): ] # Evaluate finite difference gradient fd_sensitivity = [ - (patch_perturbed - patch_orig) / heps + (patch_perturbed - patch_orig) / h_eps for patch_perturbed, patch_orig in zip( microstructure_orig_evaluations, microstructure_perturbed_evaluations, @@ -183,7 +183,7 @@ def test_macro_sensitivities(tile_class, np_rng, heps, n_test_points): patch_deriv_fd[:, jj_dim], ), ( "Implemented derivative calculation for tile class" - + f"{tile_class}, at patch {k_patch+1}/{n_patches} does not " + + f"{tile_class}, at patch {k_patch + 1}/{n_patches} does not " + "match the derivative obtained using Finite Differences at " + "the following evaluation points:\n" + str(eval_points) diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 48ae1e508..f431db1de 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -129,10 +129,9 @@ def test_tile_class(tile_class): # Check default parameter value if there is one if tile_instance._parameters_shape != (): # Assert that there is a default value - assert hasattr(tile_instance, "_default_parameter_value"), ( - f'{tile_name} must have "_default_parameter_value" as a class ' - " attribute." - ) + assert hasattr( + tile_instance, "_default_parameter_value" + ), f'{tile_name} must have "_default_parameter_value" as a class attribute.' # Check the default value's type default_value = tile_instance._default_parameter_value if isinstance(default_value, np.ndarray): @@ -252,7 +251,7 @@ def test_tile_closure(tile_class): def test_tile_derivatives( tile_class, np_rng, - heps, + h_eps, n_test_points, fd_derivative_stepsizes_and_weights, ): @@ -266,7 +265,7 @@ def test_tile_derivatives( Microtile np_rng: numpy.random._generator.Generator Default random number generator - heps: float + h_eps: float Perturbation size for finite difference evaluation. Defined in conftest.py n_test_points: int Number of testing points in the parametric domain. Defined in conftest.py @@ -329,14 +328,14 @@ def derivative_finite_difference_evaluation( for stepsize, weighting in fd_derivative_stepsizes_and_weights.items(): # Perturb parameter with respective stepsize parameters_perturbed = parameters.copy() - parameters_perturbed[:, i_parameter] += stepsize * heps + parameters_perturbed[:, i_parameter] += stepsize * h_eps # Create patches with perturbed parameter value splines_perturbed, _ = tile_creator.create_tile( parameters=parameters_perturbed, closure=closure ) fd_sensitivities += np.array( [ - weighting / heps * spl.evaluate(eval_points) + weighting / h_eps * spl.evaluate(eval_points) for spl in splines_perturbed ] ) @@ -382,12 +381,12 @@ def derivative_finite_difference_evaluation( ): message = ( f"Implemented derivative calculation for tile class {tile_class} " - f"with closure {closure}, parameter {i_parameter+1}/" - f"{n_info_per_eval_point} at patch {i_patch+1}/{n_patches} does not" - f" match the derivative obtained using Finite Differences at the " - f"following evaluation points:\n {eval_points}\nImplemented " - f"derivative:\n{deriv_orig}\nFinite Difference derivative:\n" - f"{deriv_fd}" + f"with closure {closure}, parameter {i_parameter + 1}/" + f"{n_info_per_eval_point} at patch {i_patch + 1}/{n_patches} does" + " not match the derivative obtained using Finite Differences" + f" at the following evaluation points:\n {eval_points}\n" + "Implemented derivative:\n{deriv_orig}\n" + f"Finite Difference derivative:\n{deriv_fd}" ) assert np.allclose(deriv_orig, deriv_fd), message From 742c8431cc13b248c21ac865d35251e426b0189c Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 2 Jun 2025 14:47:37 +0200 Subject: [PATCH 127/171] Fix incorrect ordering in FD calculation --- tests/test_microstructure.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 2ca2c1da9..2fd2e11ca 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -21,8 +21,9 @@ # TODO(#458): the following tiles fail the closure test CLOSURE_FAILS = { ms.tiles.HollowOctagonExtrude: "has no closure implemented", - ms.tiles.InverseCross3D: "closure is special: it does not fill the whole unit " - + "cube and does not fully cover the closing face", + ms.tiles.InverseCross3D: "closure is special: the generated tile is the inverse " + + "geometry of the closure tile of the Cross3D-tile, hence it itself does not fill" + + " the whole unit cube.", } @@ -85,7 +86,7 @@ def max_identifier(points): assert face_min_area > 1.0 - EPS, ( f"The closure of the {closure_direction}_min surface is not complete. " - f"Expected area of 1, got {face_min_area}" + f"Expected area of 1, instead got {face_min_area}" ) assert face_max_area > 1.0 - EPS, ( f"The closure of the {closure_direction}_max surface is not complete. " @@ -158,8 +159,8 @@ def test_macro_sensitivities(tile_class, np_rng, h_eps, n_test_points): fd_sensitivity = [ (patch_perturbed - patch_orig) / h_eps for patch_perturbed, patch_orig in zip( - microstructure_orig_evaluations, microstructure_perturbed_evaluations, + microstructure_orig_evaluations, ) ] @@ -179,10 +180,10 @@ def test_macro_sensitivities(tile_class, np_rng, h_eps, n_test_points): ) # Assert correctness of sensitivity assert np.allclose( - -patch_deriv_implemented[:, jj_dim], + patch_deriv_implemented[:, jj_dim], patch_deriv_fd[:, jj_dim], ), ( - "Implemented derivative calculation for tile class" + "Implemented derivative calculation for tile " + f"{tile_class}, at patch {k_patch + 1}/{n_patches} does not " + "match the derivative obtained using Finite Differences at " + "the following evaluation points:\n" From 7af98e3c0aa9c1677b28bc1acb4491dc98d15c86 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 2 Jun 2025 14:49:06 +0200 Subject: [PATCH 128/171] Fix typos and enhance error messages --- tests/conftest.py | 2 +- tests/test_microtiles.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b5fa224ae..c1068c716 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,7 +71,7 @@ def big_perturbation(): The number 0.1 is chosen arbitrarily. This value is for testing out of bounds parameter values. It is designed to add to the maximum or subtract from the - minimum bound to delibarately make the values out of the bounds.""" + minimum bound to deliberately make the values out of the bounds.""" return 0.1 diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index f431db1de..692a01958 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -122,8 +122,8 @@ def test_tile_class(tile_class): assert isinstance( getattr(tile_instance, required_variable), var_type ), ( - f"Variable {required_variable} must be of type {var_type}, but found type " - f"{type(getattr(tile_instance, required_variable))}" + f"Variable {required_variable} must be of type {var_type.__name__}, " + f"but found type {type(getattr(tile_instance, required_variable)).__name__}" ) # Check default parameter value if there is one From 8d1877fb4275ee6aecb82b064b78239e760e2658 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 2 Jun 2025 17:51:15 +0200 Subject: [PATCH 129/171] Remove HollowOctagonExtrude from list of failing tests since it passes the tests --- tests/test_microstructure.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 2fd2e11ca..cd0bc3745 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -20,7 +20,6 @@ # TODO(#458): the following tiles fail the closure test CLOSURE_FAILS = { - ms.tiles.HollowOctagonExtrude: "has no closure implemented", ms.tiles.InverseCross3D: "closure is special: the generated tile is the inverse " + "geometry of the closure tile of the Cross3D-tile, hence it itself does not fill" + " the whole unit cube.", From b6527157073dc2ee402892ee4092f8ff859ba7dd Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Mon, 20 Apr 2026 18:29:57 +0200 Subject: [PATCH 130/171] Fix(microstructure): Fix cls method calling self, add strict to zip calls --- splinepy/microstructure/tiles/tile_base.py | 2 +- tests/test_microstructure.py | 6 +++++- tests/test_microtiles.py | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 10a0ee595..88558250f 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -98,7 +98,7 @@ def dim(cls): ------- dim : int """ - return self._raise_if_not_set_else_return("_dim") + return cls._raise_if_not_set_else_return("_dim") @property def para_dim(self): diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index cd0bc3745..727f340c7 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -160,6 +160,7 @@ def test_macro_sensitivities(tile_class, np_rng, h_eps, n_test_points): for patch_perturbed, patch_orig in zip( microstructure_perturbed_evaluations, microstructure_orig_evaluations, + strict=True, ) ] @@ -170,7 +171,10 @@ def test_macro_sensitivities(tile_class, np_rng, h_eps, n_test_points): patch.evaluate(eval_points) for patch in deriv_orig.patches ] for k_patch, patch_deriv_implemented, patch_deriv_fd in zip( - range(n_patches), deriv_evaluations, fd_sensitivity + range(n_patches), + deriv_evaluations, + fd_sensitivity, + strict=True, ): # Verify derivative shapes assert patch_deriv_implemented.shape[1] == dim, ( diff --git a/tests/test_microtiles.py b/tests/test_microtiles.py index 692a01958..52d71af8b 100644 --- a/tests/test_microtiles.py +++ b/tests/test_microtiles.py @@ -377,7 +377,10 @@ def derivative_finite_difference_evaluation( ) # Check every patch for i_patch, deriv_orig, deriv_fd in zip( - range(n_patches), deriv_evaluations, fd_sensitivities + range(n_patches), + deriv_evaluations, + fd_sensitivities, + strict=True, ): message = ( f"Implemented derivative calculation for tile class {tile_class} " From 376177752dd217f99d2d293d89d401e8b7c48706 Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Mon, 20 Apr 2026 18:33:12 +0200 Subject: [PATCH 131/171] Fix(microstructure): Fix error message --- splinepy/microstructure/tiles/tile_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 88558250f..2c4975c77 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -228,8 +228,8 @@ def check_params(self, parameters): ~within_bounds.reshape(parameters.shape) ] raise ValueError( - f"The following parameters are out of bounds: {out_of_bounds}. ", - f"Expected bounds: lower: {lower_bounds} and upper: {upper_bounds}", + f"The following parameters are out of bounds: {out_of_bounds}. " + f"Expected bounds: lower: {lower_bounds} and upper: {upper_bounds}" ) return True From d8780f1b788c217d0f616f4bd056506fd8eb2a55 Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 22 Apr 2026 13:01:21 +0200 Subject: [PATCH 132/171] Make Chi-tile exempt from macro-sensitivity tests --- tests/test_microstructure.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 727f340c7..2b6713adf 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -24,6 +24,10 @@ + "geometry of the closure tile of the Cross3D-tile, hence it itself does not fill" + " the whole unit cube.", } +# TODO: the following tiles fail the macro-sensitivity testing +MACRO_FAILS = { + ms.tiles.Chi: "currently macro-sensitivity tests fail for the Chi-tile." +} @mark.parametrize("tile_class", tile_classes_with_closure) @@ -125,6 +129,14 @@ def test_macro_sensitivities(tile_class, np_rng, h_eps, n_test_points): Number of testing points in the parametric domain. Defined in conftest.py """ + # TODO: right now skip tiles which cause errors + if tile_class in MACRO_FAILS: + reason = MACRO_FAILS[tile_class] + skip( + f"Known issue: skip macro-senstivity test for {tile_class.__name__}, " + f"reason: {reason}" + ) + tile_creator = tile_class() deformation_function_orig = box(*BOX_DIMENSIONS[: tile_creator._dim]) generator = ms.microstructure.Microstructure( From 4c960ec859734ae5dfbff105e736a7beae740887 Mon Sep 17 00:00:00 2001 From: jzwar Date: Wed, 29 Apr 2026 22:16:19 +0200 Subject: [PATCH 133/171] First Try Stokes example IGA collocation --- .../iga/collocation_stokes_problem_sparse.py | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 examples/iga/collocation_stokes_problem_sparse.py diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py new file mode 100644 index 000000000..e3fd605c5 --- /dev/null +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -0,0 +1,355 @@ +r""" +This file implements a stokes test case based on the example stated in +"Finite Element Methods for Flow Problems", Donea and Huerta p.306 +chapter 6.8.1 example with analytical solution. +""" + +import numpy as np +from scipy.sparse import bmat, csr_matrix, linalg + +import splinepy as sp + +### +# Inputs +### + +# Define the dynamic viscosity +viscosity = 1 + +# Define the Geometry (simple first order unit square) +geometry = sp.BSpline( + degrees=[1, 1], + control_points=[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], + knot_vectors=[[0, 0, 1, 1], [0, 0, 1, 1]], +) + + +solution_field_pressure = sp.BSpline( + degrees=geometry.degrees, + control_points=np.ones((geometry.control_points.shape[0], 1)), + knot_vectors=geometry.knot_vectors, +) +solution_field_pressure.elevate_degrees([0, 1, 0, 1]) +solution_field_pressure.uniform_refine([1], 7) +solution_field_pressure.uniform_refine([0], 7) +# Refinement leads to quadratic spline with 10x10cps + +# Create Raviart-Thomas mixed style splines +solution_field_velocity_u = solution_field_pressure.copy() +solution_field_velocity_u.elevate_degrees([0]) +solution_field_velocity_v = solution_field_pressure.copy() +solution_field_velocity_v.elevate_degrees([1]) + +### +# Source Functions +### + + +def source_function_u(x_vec): + x = x_vec[:, 0] + y = x_vec[:, 1] + return ( + (12 - 24 * y) * x**4 + + (-24 + 48 * y) * x**3 + + (-48 * y + 72 * y**2 - 48 * y**3 + 12) * x**2 + + (-2 + 24 * y - 72 * y**2 + 48 * y**3) * x + + (1 - 4 * y + 12 * y**2 - 8 * y**3) + ) + + +def source_function_v(x_vec): + x = x_vec[:, 0] + y = x_vec[:, 1] + return ( + (8 - 48 * y + 48 * y**2) * x**3 + + (-12 + 72 * y - 72 * y**2) * x**2 + + (4 - 24 * y + 48 * y**2 - 48 * y**3 + 24 * y**4) * x + + (-12 * y**2 + 24 * y**3 - 12 * y**4) + ) + + +### +# Computation of greville abscissae +### +greville_points_u = solution_field_velocity_u.greville_abscissae() +greville_points_v = solution_field_velocity_v.greville_abscissae() +greville_points_p = solution_field_pressure.greville_abscissae() + +### +# Computation of the matrices +### +mapper_u = solution_field_velocity_u.mapper(reference=geometry) +mapper_v = solution_field_velocity_v.mapper(reference=geometry) +mapper_p = solution_field_pressure.mapper(reference=geometry) + +# FIRST ROW ---------- +# A_u_tu = mu * delta(u) +A_u_tu = -viscosity * sp.utils.data.make_matrix( + *mapper_u.basis_laplacian_and_support(greville_points_u), + solution_field_velocity_u.cps.shape[0], + as_array=False, +) +# A_v_tu = 0 +A_v_tu = None +# A_p_tu = dpdx +gradient_p, support_p = mapper_p.basis_gradient_and_support(greville_points_u) +A_p_tu = sp.utils.data.make_matrix( + gradient_p[:, :, 0], + support_p, + solution_field_pressure.cps.shape[0], + as_array=False, +) + +# SECOND ROW ---------- +# A_u_tv = 0 +A_u_tv = None +# A_v_tv = mu * delta(v) +A_v_tv = -viscosity * sp.utils.data.make_matrix( + *mapper_v.basis_laplacian_and_support(greville_points_v), + solution_field_velocity_v.cps.shape[0], + as_array=False, +) +# A_p_tv +gradient_p, support_p = mapper_p.basis_gradient_and_support(greville_points_v) +A_p_tv = sp.utils.data.make_matrix( + gradient_p[:, :, 1], + support_p, + solution_field_pressure.cps.shape[0], + as_array=False, +) +# THIRD ROW ---------- +# A_u_tp dudx +gradient_u, support_u = mapper_u.basis_gradient_and_support(greville_points_p) +A_u_tp = sp.utils.data.make_matrix( + gradient_u[:, :, 0], + support_u, + solution_field_velocity_u.cps.shape[0], + as_array=False, +) +# A_v_tp dvdx +gradient_v, support_v = mapper_v.basis_gradient_and_support(greville_points_p) +A_v_tp = sp.utils.data.make_matrix( + gradient_v[:, :, 1], + support_v, + solution_field_velocity_v.cps.shape[0], + as_array=False, +) +# A_p_tp = 0 +A_p_tp = None + + +### +# Evaluation of rhs +### +physical_points_u = geometry.evaluate(greville_points_u) +physical_points_v = geometry.evaluate(greville_points_v) +rhs_u = source_function_u(physical_points_u) +rhs_v = source_function_v(physical_points_v) +rhs_p = np.zeros(greville_points_p.shape[0]) + + +### +# Imposition of strong bcs (There must be a better solution here...) +### +# identify boundary nodes u +boundary_dof_ids = np.concatenate( + ( + solution_field_velocity_u.multi_index[0, :], + solution_field_velocity_u.multi_index[-1, :], + solution_field_velocity_u.multi_index[1:-1, 0], + solution_field_velocity_u.multi_index[1:-1, -1], + ) +) +A_u_tu[boundary_dof_ids] *= 0 # this does not change sparsity pattern +A_p_tu[boundary_dof_ids] *= 0 +A_u_tu[boundary_dof_ids, boundary_dof_ids] = 1 +rhs_u[boundary_dof_ids] = 0.0 + +# identify boundary nodes v +boundary_dof_ids = None +boundary_dof_ids = np.concatenate( + ( + solution_field_velocity_v.multi_index[0, :], + solution_field_velocity_v.multi_index[-1, :], + solution_field_velocity_v.multi_index[1:-1, 0], + solution_field_velocity_v.multi_index[1:-1, -1], + ) +) +A_v_tv[boundary_dof_ids] *= 0 # this does not change sparsity pattern +A_v_tv[boundary_dof_ids, boundary_dof_ids] = 1 +A_p_tv[boundary_dof_ids] *= 0 +rhs_v[boundary_dof_ids] = 0.0 + +# Add condition for pressure (optional) +pressure_treatment = "" +if pressure_treatment.lower() == "normalize": + n = greville_points_p.shape[0] + row_idx = np.full(n, n - 1) # all entries in last row + col_idx = np.arange(n) # columns 0..m-1 + data = np.ones(n) + A_p_tp = csr_matrix((data, (row_idx, col_idx)), shape=(n, n)) + A_u_tp[-1, :] *= 0 + A_v_tp[-1, :] *= 0 +elif pressure_treatment.lower() == "singlepoint": + n = greville_points_p.shape[0] + A_u_tp[-1, :] *= 0 + A_v_tp[-1, :] *= 0 + A_p_tp = csr_matrix(([1], ([n - 1], [n - 1])), shape=(n, n)) + rhs_p[n - 1] = 0.0 + + +### +# Solve linear system +### +rhs_all = np.concatenate([rhs_u, rhs_v, rhs_p]) +matrix_all = bmat( + [ + [A_u_tu, A_v_tu, A_p_tu], + [A_u_tv, A_v_tv, A_p_tv], + [A_u_tp, A_v_tp, A_p_tp], + ], + format=A_u_tu.format, +) +solution_vector = linalg.spsolve(matrix_all, rhs_all) + +### +# Assign solution to original splines +### +solution_field_velocity_u.control_points = np.reshape( + solution_vector[: solution_field_velocity_u.cps.shape[0]], (-1, 1) +) +solution_field_velocity_v.control_points = np.reshape( + solution_vector[ + solution_field_velocity_u.cps.shape[0] : -( + solution_field_pressure.cps.shape[0] + ) + ], + (-1, 1), +) +solution_field_pressure.control_points = np.reshape( + solution_vector[-(solution_field_pressure.cps.shape[0]) :], (-1, 1) +) + + +### +# Plot and analysis +### + +# Analytical solutions + + +def analytical_solution_u(geometry, on): + x_vec = geometry.evaluate(on) + x = x_vec[:, 0] + y = x_vec[:, 1] + return (x**2) * (1.0 - x) * (1.0 - x) * (2 * y - 6 * y**2 + 4 * y**3) + + +def error_u(solution_u): + def error_u(data, on): + sol = solution_u.evaluate(on) + ansol = analytical_solution_u(data, on) + return np.abs(sol.flat - ansol) + + return error_u + + +def analytical_solution_v(geometry, on): + x_vec = geometry.evaluate(on) + x = x_vec[:, 0] + y = x_vec[:, 1] + return -(y**2) * (1.0 - y) * (1.0 - y) * (2 * x - 6 * x**2 + 4 * x**3) + + +def error_v(solution_v): + def error_v(data, on): + sol = solution_v.evaluate(on) + ansol = analytical_solution_v(data, on) + return np.abs(sol.flat - ansol) + + return error_v + + +def analytical_solution_p(geometry, on): + x_vec = geometry.evaluate(on) + x = x_vec[:, 0] + return x * (1 - x) + + +def error_p(solution_p): + def error_p(data, on): + sol = solution_p.evaluate(on) + ansol = analytical_solution_p(data, on) + return np.abs(sol.flat - ansol) + + return error_p + + +# Plot geometry and fields +geometry.spline_data["field"] = solution_field_pressure +geometry.show_options["data"] = "field" +geometry.show_options["cmap"] = "jet" +geometry.show_options["lighting"] = "off" +geometry.show_options["scalarbar"] = True +solution_p = geometry.copy() +geometry.spline_data["field"] = solution_field_velocity_v +solution_v = geometry.copy() +geometry.spline_data["field"] = solution_field_velocity_u +solution_u = geometry.copy() +geometry.spline_data["field"] = sp.SplineDataAdaptor( + geometry, function=error_u(solution_field_velocity_u) +) +error_field_u = geometry.copy() +geometry.spline_data["field"] = sp.SplineDataAdaptor( + geometry, function=error_v(solution_field_velocity_v) +) +error_field_v = geometry.copy() +geometry.spline_data["field"] = sp.SplineDataAdaptor( + geometry, function=error_p(solution_field_pressure) +) +error_field_p = geometry.copy() + +sp.show( + ["U-Velocity", solution_u], + ["V-velocity", solution_v], + ["Pressure", solution_p], + ["|U - U_exp|", error_field_u], + ["|V - V_exp|", error_field_v], + ["|P - P_exp|", error_field_p], + knots=True, + control_points=False, +) + + +### +# Loss function u +### + +# Check Loss function +sample_points = greville_points_p +mapper_u = solution_field_velocity_u.mapper(reference=geometry) +mapper_v = solution_field_velocity_v.mapper(reference=geometry) +mapper_p = solution_field_pressure.mapper(reference=geometry) + + +def loss_function_u(data, on): + return ( + viscosity * mapper_v.laplacian(on) + - mapper_p.gradient(on)[:, 0, 1] + - source_function_v(data.evaluate(on)) + ) + + +def loss_function_v(data, on): + return ( + viscosity * mapper_u.laplacian(on) + - mapper_p.gradient(on)[:, 0, 0] + - source_function_u(data.evaluate(on)) + ) + + +geometry.spline_data["field"] = sp.SplineDataAdaptor( + geometry, function=loss_function_u +) +loss_u = geometry.copy() +loss_u.show() From 85dc64da411bfcb951166606032b5499906b1156 Mon Sep 17 00:00:00 2001 From: jzwar Date: Sat, 9 May 2026 13:39:33 +0200 Subject: [PATCH 134/171] Solve least square problem --- .../iga/collocation_stokes_problem_sparse.py | 172 ++++++++++++------ 1 file changed, 114 insertions(+), 58 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index e3fd605c5..c73240054 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -29,16 +29,16 @@ control_points=np.ones((geometry.control_points.shape[0], 1)), knot_vectors=geometry.knot_vectors, ) -solution_field_pressure.elevate_degrees([0, 1, 0, 1]) +solution_field_pressure.elevate_degrees([0, 1]) solution_field_pressure.uniform_refine([1], 7) solution_field_pressure.uniform_refine([0], 7) # Refinement leads to quadratic spline with 10x10cps # Create Raviart-Thomas mixed style splines solution_field_velocity_u = solution_field_pressure.copy() -solution_field_velocity_u.elevate_degrees([0]) +solution_field_velocity_u.elevate_degrees([1]) solution_field_velocity_v = solution_field_pressure.copy() -solution_field_velocity_v.elevate_degrees([1]) +solution_field_velocity_v.elevate_degrees([0]) ### # Source Functions @@ -51,7 +51,7 @@ def source_function_u(x_vec): return ( (12 - 24 * y) * x**4 + (-24 + 48 * y) * x**3 - + (-48 * y + 72 * y**2 - 48 * y**3 + 12) * x**2 + + (12 - 48 * y + 72 * y**2 - 48 * y**3) * x**2 + (-2 + 24 * y - 72 * y**2 + 48 * y**3) * x + (1 - 4 * y + 12 * y**2 - 8 * y**3) ) @@ -68,6 +68,14 @@ def source_function_v(x_vec): ) +def boundary_conditions_u(x_vec): + return np.zeros((x_vec.shape[0])) + + +def boundary_conditions_v(x_vec): + return np.zeros((x_vec.shape[0])) + + ### # Computation of greville abscissae ### @@ -99,6 +107,8 @@ def source_function_v(x_vec): solution_field_pressure.cps.shape[0], as_array=False, ) +# RHS +rhs_u = source_function_u(geometry.evaluate(greville_points_u)) # SECOND ROW ---------- # A_u_tv = 0 @@ -117,6 +127,9 @@ def source_function_v(x_vec): solution_field_pressure.cps.shape[0], as_array=False, ) +# RHS +rhs_v = source_function_v(geometry.evaluate(greville_points_v)) + # THIRD ROW ---------- # A_u_tp dudx gradient_u, support_u = mapper_u.basis_gradient_and_support(greville_points_p) @@ -136,23 +149,15 @@ def source_function_v(x_vec): ) # A_p_tp = 0 A_p_tp = None - - -### -# Evaluation of rhs -### -physical_points_u = geometry.evaluate(greville_points_u) -physical_points_v = geometry.evaluate(greville_points_v) -rhs_u = source_function_u(physical_points_u) -rhs_v = source_function_v(physical_points_v) +# RHS rhs_p = np.zeros(greville_points_p.shape[0]) ### -# Imposition of strong bcs (There must be a better solution here...) +# Imposition of BCs ### -# identify boundary nodes u -boundary_dof_ids = np.concatenate( +# For U-Velocity +boundary_evaluation_point_ids_v = np.concatenate( ( solution_field_velocity_u.multi_index[0, :], solution_field_velocity_u.multi_index[-1, :], @@ -160,14 +165,21 @@ def source_function_v(x_vec): solution_field_velocity_u.multi_index[1:-1, -1], ) ) -A_u_tu[boundary_dof_ids] *= 0 # this does not change sparsity pattern -A_p_tu[boundary_dof_ids] *= 0 -A_u_tu[boundary_dof_ids, boundary_dof_ids] = 1 -rhs_u[boundary_dof_ids] = 0.0 - -# identify boundary nodes v -boundary_dof_ids = None -boundary_dof_ids = np.concatenate( +boundary_evaluation_points_u = greville_points_u[ + boundary_evaluation_point_ids_v] + +# Create matrices and RHS +A_u_tpbcu = sp.utils.data.make_matrix( + *solution_field_velocity_u.basis_and_support(boundary_evaluation_points_u), + solution_field_velocity_u.cps.shape[0], + as_array=False, +) +A_v_tpbcu = None +A_p_tpbcu = None +rhs_tpbcu = boundary_conditions_u(boundary_evaluation_points_u) + +# For V-velocity +boundary_evaluation_point_ids_v = np.concatenate( ( solution_field_velocity_v.multi_index[0, :], solution_field_velocity_v.multi_index[-1, :], @@ -175,42 +187,59 @@ def source_function_v(x_vec): solution_field_velocity_v.multi_index[1:-1, -1], ) ) -A_v_tv[boundary_dof_ids] *= 0 # this does not change sparsity pattern -A_v_tv[boundary_dof_ids, boundary_dof_ids] = 1 -A_p_tv[boundary_dof_ids] *= 0 -rhs_v[boundary_dof_ids] = 0.0 - -# Add condition for pressure (optional) -pressure_treatment = "" -if pressure_treatment.lower() == "normalize": - n = greville_points_p.shape[0] - row_idx = np.full(n, n - 1) # all entries in last row - col_idx = np.arange(n) # columns 0..m-1 - data = np.ones(n) - A_p_tp = csr_matrix((data, (row_idx, col_idx)), shape=(n, n)) - A_u_tp[-1, :] *= 0 - A_v_tp[-1, :] *= 0 -elif pressure_treatment.lower() == "singlepoint": - n = greville_points_p.shape[0] - A_u_tp[-1, :] *= 0 - A_v_tp[-1, :] *= 0 - A_p_tp = csr_matrix(([1], ([n - 1], [n - 1])), shape=(n, n)) - rhs_p[n - 1] = 0.0 +boundary_evaluation_points_v = greville_points_v[ + boundary_evaluation_point_ids_v] + +# Create matrices and RHS +A_u_tpbcv = None +A_v_tpbcv = sp.utils.data.make_matrix( + *solution_field_velocity_v.basis_and_support(boundary_evaluation_points_v), + solution_field_velocity_v.cps.shape[0], + as_array=False, +) +A_p_tpbcv = None +rhs_tpbcv = boundary_conditions_u(boundary_evaluation_points_u) + +# For Pressure Field (redundant?) +impose_pressure_bcs = False +if impose_pressure_bcs: + boundary_evaluation_point_ids_p = np.concatenate( + ( + solution_field_pressure.multi_index[0, :], + solution_field_pressure.multi_index[-1, :], + ) + ) + boundary_evaluation_points_p = greville_points_p[ + boundary_evaluation_point_ids_p] + + # Create matrices and RHS + A_u_tpbcp = None + A_v_tpbcp = None + A_p_tpbcp = sp.utils.data.make_matrix( + *solution_field_pressure.basis_and_support(boundary_evaluation_points_p), + solution_field_pressure.cps.shape[0], + as_array=False, + ) + rhs_tpbcp = boundary_conditions_u(boundary_evaluation_points_p) ### # Solve linear system ### -rhs_all = np.concatenate([rhs_u, rhs_v, rhs_p]) -matrix_all = bmat( - [ - [A_u_tu, A_v_tu, A_p_tu], - [A_u_tv, A_v_tv, A_p_tv], - [A_u_tp, A_v_tp, A_p_tp], - ], - format=A_u_tu.format, -) -solution_vector = linalg.spsolve(matrix_all, rhs_all) +rhs_all = np.concatenate([rhs_u, rhs_v, rhs_p, rhs_tpbcu, rhs_tpbcv]) +block_matrices = [ + [A_u_tu, A_v_tu, A_p_tu], + [A_u_tv, A_v_tv, A_p_tv], + [A_u_tp, A_v_tp, A_p_tp], + [A_u_tpbcu, A_v_tpbcu, A_p_tpbcu], + [A_u_tpbcv, A_v_tpbcv, A_p_tpbcv], +] +if impose_pressure_bcs: + rhs_all = np.concatenate([rhs_all, rhs_tpbcp + ]) + block_matrices.append([A_u_tpbcp, A_v_tpbcp, A_p_tpbcp]) +matrix_all = bmat(block_matrices, format=A_u_tu.format) +solution_vector, istop, itn, r1norm = linalg.lsqr(matrix_all, rhs_all)[:4] ### # Assign solution to original splines @@ -220,14 +249,14 @@ def source_function_v(x_vec): ) solution_field_velocity_v.control_points = np.reshape( solution_vector[ - solution_field_velocity_u.cps.shape[0] : -( + solution_field_velocity_u.cps.shape[0]: -( solution_field_pressure.cps.shape[0] ) ], (-1, 1), ) solution_field_pressure.control_points = np.reshape( - solution_vector[-(solution_field_pressure.cps.shape[0]) :], (-1, 1) + solution_vector[-(solution_field_pressure.cps.shape[0]):], (-1, 1) ) @@ -348,8 +377,35 @@ def loss_function_v(data, on): ) +def loss_function_p(data, on): + return ( + mapper_u.gradient(on)[:, 0, 0] + + mapper_v.gradient(on)[:, 0, 1] + ) + + geometry.spline_data["field"] = sp.SplineDataAdaptor( geometry, function=loss_function_u ) loss_u = geometry.copy() -loss_u.show() +geometry.spline_data["field"] = sp.SplineDataAdaptor( + geometry, function=loss_function_v +) +loss_v = geometry.copy() +geometry.spline_data["field"] = sp.SplineDataAdaptor( + geometry, function=loss_function_p +) +loss_p = geometry.copy() +sp.show( + ["U-Velocity", solution_u], + ["V-velocity", solution_v], + ["Pressure", solution_p], + ["|U - U_exp|", error_field_u], + ["|V - V_exp|", error_field_v], + ["|P - P_exp|", error_field_p], + ["|R(u)|", loss_u], + ["|R(v)|", loss_v], + ["|R(p)|", loss_p], + knots=True, + control_points=False, +) From a90b1f3e6f2c5a799b2823542665554786f56940 Mon Sep 17 00:00:00 2001 From: jzwar Date: Sun, 10 May 2026 14:57:37 +0200 Subject: [PATCH 135/171] Make example with more options --- .../iga/collocation_stokes_problem_sparse.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index c73240054..e5cb95b54 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -5,7 +5,9 @@ """ import numpy as np -from scipy.sparse import bmat, csr_matrix, linalg +from scipy.sparse import bmat, linalg + +import matplotlib.pyplot as plt import splinepy as sp @@ -15,6 +17,8 @@ # Define the dynamic viscosity viscosity = 1 +mass_factor = 1e-1 # Scale second loss function +impose_pressure_bcs = False # Impose BCS for pressure # Define the Geometry (simple first order unit square) geometry = sp.BSpline( @@ -134,7 +138,7 @@ def boundary_conditions_v(x_vec): # A_u_tp dudx gradient_u, support_u = mapper_u.basis_gradient_and_support(greville_points_p) A_u_tp = sp.utils.data.make_matrix( - gradient_u[:, :, 0], + mass_factor * gradient_u[:, :, 0], support_u, solution_field_velocity_u.cps.shape[0], as_array=False, @@ -142,7 +146,7 @@ def boundary_conditions_v(x_vec): # A_v_tp dvdx gradient_v, support_v = mapper_v.basis_gradient_and_support(greville_points_p) A_v_tp = sp.utils.data.make_matrix( - gradient_v[:, :, 1], + mass_factor * gradient_v[:, :, 1], support_v, solution_field_velocity_v.cps.shape[0], as_array=False, @@ -150,7 +154,7 @@ def boundary_conditions_v(x_vec): # A_p_tp = 0 A_p_tp = None # RHS -rhs_p = np.zeros(greville_points_p.shape[0]) +rhs_p = mass_factor * np.zeros(greville_points_p.shape[0]) ### @@ -201,7 +205,6 @@ def boundary_conditions_v(x_vec): rhs_tpbcv = boundary_conditions_u(boundary_evaluation_points_u) # For Pressure Field (redundant?) -impose_pressure_bcs = False if impose_pressure_bcs: boundary_evaluation_point_ids_p = np.concatenate( ( @@ -241,6 +244,12 @@ def boundary_conditions_v(x_vec): matrix_all = bmat(block_matrices, format=A_u_tu.format) solution_vector, istop, itn, r1norm = linalg.lsqr(matrix_all, rhs_all)[:4] + +# Plot Matrix +plt.spy(matrix_all) +plt.show() + + ### # Assign solution to original splines ### @@ -338,17 +347,6 @@ def error_p(data, on): ) error_field_p = geometry.copy() -sp.show( - ["U-Velocity", solution_u], - ["V-velocity", solution_v], - ["Pressure", solution_p], - ["|U - U_exp|", error_field_u], - ["|V - V_exp|", error_field_v], - ["|P - P_exp|", error_field_p], - knots=True, - control_points=False, -) - ### # Loss function u @@ -378,7 +376,7 @@ def loss_function_v(data, on): def loss_function_p(data, on): - return ( + return mass_factor * ( mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1] ) From b91dc0642fa004e44a477d6cf55e4ca8a92baf8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 13:01:37 +0000 Subject: [PATCH 136/171] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../iga/collocation_stokes_problem_sparse.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index e5cb95b54..ef2e48de8 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -4,11 +4,10 @@ chapter 6.8.1 example with analytical solution. """ +import matplotlib.pyplot as plt import numpy as np from scipy.sparse import bmat, linalg -import matplotlib.pyplot as plt - import splinepy as sp ### @@ -73,11 +72,11 @@ def source_function_v(x_vec): def boundary_conditions_u(x_vec): - return np.zeros((x_vec.shape[0])) + return np.zeros(x_vec.shape[0]) def boundary_conditions_v(x_vec): - return np.zeros((x_vec.shape[0])) + return np.zeros(x_vec.shape[0]) ### @@ -170,7 +169,8 @@ def boundary_conditions_v(x_vec): ) ) boundary_evaluation_points_u = greville_points_u[ - boundary_evaluation_point_ids_v] + boundary_evaluation_point_ids_v +] # Create matrices and RHS A_u_tpbcu = sp.utils.data.make_matrix( @@ -192,7 +192,8 @@ def boundary_conditions_v(x_vec): ) ) boundary_evaluation_points_v = greville_points_v[ - boundary_evaluation_point_ids_v] + boundary_evaluation_point_ids_v +] # Create matrices and RHS A_u_tpbcv = None @@ -213,13 +214,16 @@ def boundary_conditions_v(x_vec): ) ) boundary_evaluation_points_p = greville_points_p[ - boundary_evaluation_point_ids_p] + boundary_evaluation_point_ids_p + ] # Create matrices and RHS A_u_tpbcp = None A_v_tpbcp = None A_p_tpbcp = sp.utils.data.make_matrix( - *solution_field_pressure.basis_and_support(boundary_evaluation_points_p), + *solution_field_pressure.basis_and_support( + boundary_evaluation_points_p + ), solution_field_pressure.cps.shape[0], as_array=False, ) @@ -238,8 +242,7 @@ def boundary_conditions_v(x_vec): [A_u_tpbcv, A_v_tpbcv, A_p_tpbcv], ] if impose_pressure_bcs: - rhs_all = np.concatenate([rhs_all, rhs_tpbcp - ]) + rhs_all = np.concatenate([rhs_all, rhs_tpbcp]) block_matrices.append([A_u_tpbcp, A_v_tpbcp, A_p_tpbcp]) matrix_all = bmat(block_matrices, format=A_u_tu.format) solution_vector, istop, itn, r1norm = linalg.lsqr(matrix_all, rhs_all)[:4] @@ -258,14 +261,14 @@ def boundary_conditions_v(x_vec): ) solution_field_velocity_v.control_points = np.reshape( solution_vector[ - solution_field_velocity_u.cps.shape[0]: -( + solution_field_velocity_u.cps.shape[0] : -( solution_field_pressure.cps.shape[0] ) ], (-1, 1), ) solution_field_pressure.control_points = np.reshape( - solution_vector[-(solution_field_pressure.cps.shape[0]):], (-1, 1) + solution_vector[-(solution_field_pressure.cps.shape[0]) :], (-1, 1) ) @@ -377,8 +380,7 @@ def loss_function_v(data, on): def loss_function_p(data, on): return mass_factor * ( - mapper_u.gradient(on)[:, 0, 0] - + mapper_v.gradient(on)[:, 0, 1] + mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1] ) From 68a8d9515a65f1b8e40652d44570404ed38ce5e8 Mon Sep 17 00:00:00 2001 From: Daniel Wolff Date: Wed, 13 May 2026 20:01:18 +0200 Subject: [PATCH 137/171] Slight modifications to stokes collocation example --- .../iga/collocation_stokes_problem_sparse.py | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index ef2e48de8..775d3fc69 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -16,7 +16,7 @@ # Define the dynamic viscosity viscosity = 1 -mass_factor = 1e-1 # Scale second loss function +mass_factor = 1e-1 # Weight the incompressibility equations in the LSQ solve impose_pressure_bcs = False # Impose BCS for pressure # Define the Geometry (simple first order unit square) @@ -39,9 +39,11 @@ # Create Raviart-Thomas mixed style splines solution_field_velocity_u = solution_field_pressure.copy() -solution_field_velocity_u.elevate_degrees([1]) +# For div-conforming tensor-product spaces in 2D, the x-velocity has +# one higher degree in x, and the y-velocity has one higher degree in y. +solution_field_velocity_u.elevate_degrees([0]) solution_field_velocity_v = solution_field_pressure.copy() -solution_field_velocity_v.elevate_degrees([0]) +solution_field_velocity_v.elevate_degrees([1]) ### # Source Functions @@ -79,6 +81,10 @@ def boundary_conditions_v(x_vec): return np.zeros(x_vec.shape[0]) +def boundary_conditions_p(x_vec): + return np.zeros(x_vec.shape[0]) + + ### # Computation of greville abscissae ### @@ -160,7 +166,7 @@ def boundary_conditions_v(x_vec): # Imposition of BCs ### # For U-Velocity -boundary_evaluation_point_ids_v = np.concatenate( +boundary_evaluation_point_ids_u = np.concatenate( ( solution_field_velocity_u.multi_index[0, :], solution_field_velocity_u.multi_index[-1, :], @@ -169,7 +175,7 @@ def boundary_conditions_v(x_vec): ) ) boundary_evaluation_points_u = greville_points_u[ - boundary_evaluation_point_ids_v + boundary_evaluation_point_ids_u ] # Create matrices and RHS @@ -203,7 +209,7 @@ def boundary_conditions_v(x_vec): as_array=False, ) A_p_tpbcv = None -rhs_tpbcv = boundary_conditions_u(boundary_evaluation_points_u) +rhs_tpbcv = boundary_conditions_v(boundary_evaluation_points_v) # For Pressure Field (redundant?) if impose_pressure_bcs: @@ -227,7 +233,7 @@ def boundary_conditions_v(x_vec): solution_field_pressure.cps.shape[0], as_array=False, ) - rhs_tpbcp = boundary_conditions_u(boundary_evaluation_points_p) + rhs_tpbcp = boundary_conditions_p(boundary_evaluation_points_p) ### @@ -245,7 +251,14 @@ def boundary_conditions_v(x_vec): rhs_all = np.concatenate([rhs_all, rhs_tpbcp]) block_matrices.append([A_u_tpbcp, A_v_tpbcp, A_p_tpbcp]) matrix_all = bmat(block_matrices, format=A_u_tu.format) -solution_vector, istop, itn, r1norm = linalg.lsqr(matrix_all, rhs_all)[:4] +solution_vector, istop, itn, r1norm = linalg.lsqr( + matrix_all, + rhs_all, + atol=1e-12, + btol=1e-12, + iter_lim=100 * matrix_all.shape[1], +)[:4] +print(f"LSQR istop={istop}, iterations={itn}, residual={r1norm:.3e}") # Plot Matrix @@ -317,11 +330,24 @@ def analytical_solution_p(geometry, on): return x * (1 - x) +# if we do not impose pressure BCs, we need to align the computed pressure +# field with the analytical solution because the pressure field is only +# determined up to a constant +pressure_error_offset = 0.0 +if not impose_pressure_bcs: + # we choose the offset as the minimum value of the computed pressure field + # such that the computed pressure field is (theoretically) non-negative + pressure_error_offset = solution_field_pressure.evaluate( + greville_points_p + ).min() + + def error_p(solution_p): def error_p(data, on): sol = solution_p.evaluate(on) + corr_sol = sol.flat - pressure_error_offset ansol = analytical_solution_p(data, on) - return np.abs(sol.flat - ansol) + return np.abs(corr_sol - ansol) return error_p @@ -363,18 +389,18 @@ def error_p(data, on): def loss_function_u(data, on): - return ( - viscosity * mapper_v.laplacian(on) - - mapper_p.gradient(on)[:, 0, 1] - - source_function_v(data.evaluate(on)) + return np.abs( + -viscosity * mapper_u.laplacian(on).ravel() + + mapper_p.gradient(on)[:, 0, 0] + - source_function_u(data.evaluate(on)) ) def loss_function_v(data, on): - return ( - viscosity * mapper_u.laplacian(on) - - mapper_p.gradient(on)[:, 0, 0] - - source_function_u(data.evaluate(on)) + return np.abs( + -viscosity * mapper_v.laplacian(on).ravel() + + mapper_p.gradient(on)[:, 0, 1] + - source_function_v(data.evaluate(on)) ) From b8aee96ee07a1aba8b273575299931952d169b36 Mon Sep 17 00:00:00 2001 From: jzwar Date: Sun, 17 May 2026 13:50:10 +0200 Subject: [PATCH 138/171] Final Touches --- examples/iga/collocation_stokes_problem_sparse.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index 775d3fc69..4433e3257 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -33,9 +33,9 @@ knot_vectors=geometry.knot_vectors, ) solution_field_pressure.elevate_degrees([0, 1]) +# Refinement leads to quadratic spline with 10x10cps solution_field_pressure.uniform_refine([1], 7) solution_field_pressure.uniform_refine([0], 7) -# Refinement leads to quadratic spline with 10x10cps # Create Raviart-Thomas mixed style splines solution_field_velocity_u = solution_field_pressure.copy() @@ -82,7 +82,8 @@ def boundary_conditions_v(x_vec): def boundary_conditions_p(x_vec): - return np.zeros(x_vec.shape[0]) + x = x_vec[:, 0] + return x * (1.0 - x) ### @@ -213,6 +214,8 @@ def boundary_conditions_p(x_vec): # For Pressure Field (redundant?) if impose_pressure_bcs: + # boundary conditions only applied to the sides x=0 and x=1 + # To apply to all 4 sides see bc for velocity field boundary_evaluation_point_ids_p = np.concatenate( ( solution_field_pressure.multi_index[0, :], @@ -247,9 +250,12 @@ def boundary_conditions_p(x_vec): [A_u_tpbcu, A_v_tpbcu, A_p_tpbcu], [A_u_tpbcv, A_v_tpbcv, A_p_tpbcv], ] + +# L2-imposition of boundary conditions for the pressure field (optional) if impose_pressure_bcs: rhs_all = np.concatenate([rhs_all, rhs_tpbcp]) block_matrices.append([A_u_tpbcp, A_v_tpbcp, A_p_tpbcp]) + matrix_all = bmat(block_matrices, format=A_u_tu.format) solution_vector, istop, itn, r1norm = linalg.lsqr( matrix_all, @@ -257,6 +263,7 @@ def boundary_conditions_p(x_vec): atol=1e-12, btol=1e-12, iter_lim=100 * matrix_all.shape[1], + # calc_var=True )[:4] print(f"LSQR istop={istop}, iterations={itn}, residual={r1norm:.3e}") @@ -345,7 +352,7 @@ def analytical_solution_p(geometry, on): def error_p(solution_p): def error_p(data, on): sol = solution_p.evaluate(on) - corr_sol = sol.flat - pressure_error_offset + corr_sol = sol.flatten() - pressure_error_offset ansol = analytical_solution_p(data, on) return np.abs(corr_sol - ansol) @@ -404,7 +411,7 @@ def loss_function_v(data, on): ) -def loss_function_p(data, on): +def loss_function_p(_data, on): return mass_factor * ( mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1] ) From 1a2762c1867d0a262b90aede4694121158b021d5 Mon Sep 17 00:00:00 2001 From: Daniel Wolff Date: Thu, 21 May 2026 16:16:26 +0200 Subject: [PATCH 139/171] Add nitpicks from AI code review --- .../iga/collocation_stokes_problem_sparse.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index 4433e3257..1cc4497ab 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -307,12 +307,12 @@ def analytical_solution_u(geometry, on): def error_u(solution_u): - def error_u(data, on): + def _error_u(data, on): sol = solution_u.evaluate(on) ansol = analytical_solution_u(data, on) return np.abs(sol.flat - ansol) - return error_u + return _error_u def analytical_solution_v(geometry, on): @@ -323,12 +323,12 @@ def analytical_solution_v(geometry, on): def error_v(solution_v): - def error_v(data, on): + def _error_v(data, on): sol = solution_v.evaluate(on) ansol = analytical_solution_v(data, on) return np.abs(sol.flat - ansol) - return error_v + return _error_v def analytical_solution_p(geometry, on): @@ -350,13 +350,13 @@ def analytical_solution_p(geometry, on): def error_p(solution_p): - def error_p(data, on): + def _error_p(data, on): sol = solution_p.evaluate(on) corr_sol = sol.flatten() - pressure_error_offset ansol = analytical_solution_p(data, on) return np.abs(corr_sol - ansol) - return error_p + return _error_p # Plot geometry and fields @@ -412,8 +412,10 @@ def loss_function_v(data, on): def loss_function_p(_data, on): - return mass_factor * ( - mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1] + return np.abs( + mass_factor * ( + mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1] + ) ) From b92b922fa3a1c2d4407c10e470a49a1011b2d7d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 14:16:49 +0000 Subject: [PATCH 140/171] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/iga/collocation_stokes_problem_sparse.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/iga/collocation_stokes_problem_sparse.py b/examples/iga/collocation_stokes_problem_sparse.py index 1cc4497ab..aa00655eb 100644 --- a/examples/iga/collocation_stokes_problem_sparse.py +++ b/examples/iga/collocation_stokes_problem_sparse.py @@ -413,9 +413,8 @@ def loss_function_v(data, on): def loss_function_p(_data, on): return np.abs( - mass_factor * ( - mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1] - ) + mass_factor + * (mapper_u.gradient(on)[:, 0, 0] + mapper_v.gradient(on)[:, 0, 1]) ) From 89bf8093d7919a55af9d4930cbbdfbcbb92db59d Mon Sep 17 00:00:00 2001 From: Jaewook Lee Date: Tue, 27 Feb 2024 16:49:44 +0100 Subject: [PATCH 141/171] add FieldIntegrator --- splinepy/helpme/integrate.py | 211 +++++++++++++++++++++++++++++++---- 1 file changed, 191 insertions(+), 20 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 4fdec2440..287d48fdf 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -2,6 +2,7 @@ import numpy as _np +from splinepy._base import SplinepyBase as _SplinepyBase from splinepy.utils.data import cartesian_product as _cartesian_product @@ -43,9 +44,7 @@ def measure(spline_patch, positions): def measure(spline_patch, positions): jacs = spline_patch.jacobian(positions) - return _np.sqrt( - _np.linalg.det(_np.einsum("oji,ojk->oik", jacs, jacs)) - ) + return _np.sqrt(_np.linalg.det(_np.einsum("oji,ojk->oik", jacs, jacs))) return measure @@ -53,6 +52,16 @@ def measure(spline_patch, positions): raise ValueError("`Volume` not supported if para_dim > dim") +def _default_quadrature_orders(spline): + expected_degree = spline.degrees * spline.para_dim + 1 + if spline.is_rational: + expected_degree += 2 + spline._logd("Integration on rational spline is only approximation") + + # Gauss-Legendre is exact for polynomials 2*n-1 + return _np.ceil((expected_degree + 1) * 0.5).astype("int") + + def _get_quadrature_information(spline, orders=None): """ Select appropriate integration order (gauss-legendre) @@ -91,21 +100,12 @@ def _get_quadrature_information(spline, orders=None): # Determine integration orders if orders is None: - expected_degree = spline.degrees * spline.para_dim + 1 - if spline.is_rational: - expected_degree += 2 - spline._logd( - "Integration on rational spline is only approximation" - ) + quad_orders = _default_quadrature_orders(spline) - # Gauss-Legendre is exact for polynomials 2*n-1 - quad_orders = _np.ceil((expected_degree + 1) * 0.5).astype("int") else: quad_orders = _np.ascontiguousarray(orders, dtype=int).flatten() if quad_orders.size != spline.para_dim: - raise ValueError( - "Integration order must be array of size para_dim" - ) + raise ValueError("Integration order must be array of size para_dim") for order in quad_orders: # Get legendre quadratuer points @@ -150,10 +150,7 @@ def volume(spline, orders=None): # Calculate Volume if spline.has_knot_vectors: volume = _np.sum( - [ - _np.sum(meas(b, positions) * weights) - for b in spline.extract.beziers() - ] + [_np.sum(meas(b, positions) * weights) for b in spline.extract.beziers()] ) else: volume = _np.sum(meas(spline, positions) * weights) @@ -243,15 +240,53 @@ def parametric_function( Parameters ---------- spline : Spline - (self if called via integrator) + The geometry over which the function is integrated function : Callable + The user-defined function to integrate. It takes points in the parametric + dimension as input and outputs a scalar or an array of scalars. orders : optional + Quadrature orders for numerical integration Returns ------- integral : np.ndarray """ - return _user_function(spline, function, orders=orders, physical=False) + from splinepy.spline import Spline as _Spline + + # Check input type + if not isinstance(spline, _Spline): + raise NotImplementedError("integration only works for splines") + + # Retrieve aux info + meas = _get_integral_measure(spline) + positions, weights = _get_quadrature_information(spline, orders) + + # Calculate Volume + if spline.has_knot_vectors: + # positions must be mapped into each knot-element + para_view = spline.create.parametric_view(axes=False) + + # get initial shape + initial = function([positions[0]]) + result = _np.zeros(initial.shape[1]) + for bezier_element in para_view.extract.beziers(): + quad_positions = bezier_element.evaluate(positions) + result += _np.einsum( + "i...,i,i->...", + function(quad_positions), + meas(spline, quad_positions), + weights, + optimize=True, + ) + + else: + result = _np.einsum( + "i...,i,i->...", + function(positions), + meas(spline, positions), + weights, + ) + return result def physical_function( @@ -312,3 +347,139 @@ def parametric_function(self, *args, **kwargs): @_wraps(physical_function) def physical_function(self, *args, **kwargs): return physical_function(self._helpee, *args, **kwargs) + + +class FieldIntegrator(_SplinepyBase): + __slots__ = ( + "_positions", + "_weights", + "_helpee", + "_orders", + "_supports", + "_bezier_patches", + "_global_positions", + "_jacobians", + "_jacobian_weights", + ) + + def __init__(self, spline, orders=None): + """ """ + self._helpee = spline + + self.reset(orders) + + def reset(self, orders=None): + """ """ + if orders is None: + self._orders = _default_quadrature_orders(self._helpee) + else: + self._orders = orders + + self._positions, self._weights = _get_quadrature_information( + self._helpee, self._orders + ) + + self._bezier_patches = None + self._global_positions = None + self._supports = None + self._jacobians = None + self._jacobian_weights = None + + @property + def positions(self): + """ + Normalized Quadrature positions. Can use this value for + """ + return self._positions + + @property + def global_positions(self): + """ + Quadrature points in global position + """ + if self._global_positions is not None: + return self._global_positions + + # TODO: clamped knot vector check once it's merged + lower_bounds_per_dim = [] + span_scales_per_dim = [] + for ukv in self._helpee.unique_knots: + lower_bounds_per_dim.append(ukv[:-1]) + span_scales_per_dim.append(_np.diff(ukv)) + lower_bounds = _cartesian_product(lower_bounds_per_dim, reverse=True) + span_scales = _cartesian_product(span_scales_per_dim, reverse=True) + + # add lower bound as offsets using np.broadcast rules + n_quads, dim = self.positions.shape + n_elem = len(lower_bounds) + + # create normalized quad points for each element + self._global_positions = _np.tile(self.positions, (n_elem, 1)).reshape( + n_elem, n_quads, dim + ) + # scale them + self._global_positions *= span_scales.reshape(n_elem, 1, dim) + # apply offset + self._global_positions += lower_bounds.reshape(n_elem, 1, dim) + + return self._global_positions + + @property + def supports(self): + """ """ + if self._supports is not None: + return self._supports + + self._supports = self._helpee.supports(self.global_positions) + return self._supports + + @property + def quadrature_weights(self): + """ """ + return self._weights + + @property + def bezier_patches(self): + """Returns extracted bezier patches. + These beziers can be used to compute jacobians and weights thereof. + """ + if self._bezier_patches is not None: + return self._bezier_patches + + self._bezier_patches = self._helpee.extract.beziers() + return self._bezier_patches + + @property + def jacobians(self): + if self._jacobians is not None: + return self._jacobians + + self._jacobians = [bp.jacobian(self.positions) for bp in self.bezier_patches] + + return self._jacobians + + @property + def jacobian_inverses(self): + if self._jacobian_inverses is not None: + return self._jacobian_inverses + + # will raise if this isn't square + self._jacobian_inverses = [_np.linalg.inv(j) for j in self.jacobians] + + return self._jacobian_inverses + + @property + def jacobian_weights(self): + """ + Jacobian determinants if jacobian is a square matrix. + Otherwise, + """ + if self._jacobian_weights is not None: + return self._jacobian_weights + + meas = _get_integral_measure(self._helpee) + self._jacobian_weights = [ + meas(bp, self.positions) for bp in self.bezier_patches + ] + + return self._jacobian_weights From 24f2b76872432abc551ffe56c9de5fe8f313ec24 Mon Sep 17 00:00:00 2001 From: Jaewook Lee Date: Tue, 27 Feb 2024 17:05:35 +0100 Subject: [PATCH 142/171] add assemble base --- splinepy/helpme/integrate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 287d48fdf..53c9dfeef 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -483,3 +483,13 @@ def jacobian_weights(self): ] return self._jacobian_weights + + @property + def assemble_matrix(self, function, dim, matrix=None): + """ """ + pass + + @property + def assemble_vector(self, function, dim, matrix=None): + """ """ + pass From f18a6c2cfad1dce26f015a890a3e2fa78ef678aa Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 1 Aug 2024 16:29:45 +0200 Subject: [PATCH 143/171] add Transformation class --- splinepy/helpme/integrate.py | 307 ++++++++++++++++++++++++++++++++++- 1 file changed, 305 insertions(+), 2 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 53c9dfeef..212a06f32 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -6,6 +6,310 @@ from splinepy.utils.data import cartesian_product as _cartesian_product +class Transformation: + __slots__ = ( + "_spline", + "_para_dim", + "_ukv", + "_n_elems_per_dim", + "_quad_positions", + "_quad_weights", + "_all_element_quad_points", + "_all_jacobians", + "_all_jacobian_inverses", + "_all_jacobian_determinants", + ) + + def __init__(self, spline): + self._spline = spline + self._para_dim = spline.para_dim + if self._para_dim == 3: + raise NotImplementedError("Not yet tested for 3D") + + self._ukv = self._spline.unique_knots + self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + + # Gauss-Legendre quadrature points and weights + max_order = int(_np.max(self._spline.degrees)) + quad_positions, quad_weights = _np.polynomial.legendre.leggauss( + deg=max_order + ) + self._quad_positions = _cartesian_product( + [quad_positions for _ in range(self._para_dim)] + ) + self._quad_weights = _cartesian_product( + [quad_weights for _ in range(self._para_dim)] + ) + self._quad_weights = _np.prod(self._quad_weights, axis=1) + + self._all_element_quad_points = None + self._all_jacobians = None + self._all_jacobian_inverses = None + self._all_jacobian_determinants = None + + def check_element_id_validity(self, element_id): + """Check if given element ID is valid + + Parameters + ----------- + element_id: int + ID of element in spline's element. ID-array is 1D + """ + assert element_id >= 0 + assert element_id < _np.prod(self._n_elems_per_dim) + + @property + def all_quad_points(self): + """Quadrature points of all elements. + Dimensions [, , 2]""" + return self._all_element_quad_points + + @property + def all_jacobians(self): + """Jacobians of all elements. + Dimensions [ , , ]""" + return self._all_jacobians + + @property + def all_jacobian_inverses(self): + """Inverses of Jacobians of all elements. + Dimensions [ , , ]""" + return self._all_jacobian_inverses + + @property + def all_jacobian_determinants(self): + """Determinants of Jacobians of all elements. + Dimensions [ ]""" + return self._all_jacobian_determinants + + @property + def quadrature_weights(self): + return self._quad_weights + + def get_element_grid_id(self, element_id): + """Compute element ID in grid + + Parameters + ---------- + element_id: int + ID of element of spline + + Returns + --------- + element_grid_id: list + ID of element in grid + """ + if self._para_dim == 3: + raise NotImplementedError( + "Element grid ID not yet implemented for 3D" + ) + + n_elems_x = self._n_elems_per_dim[0] + grid_id = [element_id % n_elems_x, element_id // n_elems_x] + + return grid_id + + def get_element_quad_points(self, element_id): + """For given element computes quad points + + Parameters + ----------- + element_id: int + ID of element in spline's element + + Returns + ----------- + element_quad_points: np.ndarray + Quadrature points for element + """ + self.check_element_id_validity(element_id) + + if self._all_element_quad_points is not None: + return self._all_element_quad_points[element_id] + + element_grid_id = self.get_element_grid_id(element_id) + + element_corner_points = _np.vstack( + [ + ukv_dim[e_dim_id : (e_dim_id + 2)] + for ukv_dim, e_dim_id in zip(self._ukv, element_grid_id) + ] + ) + element_lengths = _np.diff(element_corner_points, axis=1).ravel() + element_midpoints = _np.mean(element_corner_points, axis=1) + + return self._quad_positions / 2 * element_lengths + element_midpoints + + def jacobian(self, element_id): + """Return Jacobian of single element at quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid + + Returns + --------- + element_jacobian: np.ndarray + Jacobian of element evaluated at quadrature points + """ + if self._all_jacobians is not None: + return self._all_jacobians[element_id] + + element_quad_points = self.get_element_quad_points(element_id) + + return self._spline.jacobian(element_quad_points) + + def jacobian_inverse(self, element_id): + """Return inverse of Jacobian of single element, evaluated at quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid + + Returns + --------- + element_inverse_jacobian: np.ndarray + Inverse of Jacobian of element evaluated at quadrature points + """ + if self._all_jacobian_inverses is not None: + return self._all_jacobian_inverses[element_id] + + element_jacobians = self.jacobian(element_id) + element_jacobian_inverse = _np.stack( + [ + _np.linalg.inv(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + + return element_jacobian_inverse + + def jacobian_determinant(self, element_id): + """Return determinant of Jacobian of single element, evaluated at + quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid + + Returns + --------- + element_jacobian_determinant: np.ndarray + Determinant of Jacobian of element evaluated at quadrature points + """ + if self._all_jacobian_determinants is not None: + return self._all_jacobian_determinants[element_id] + + element_jacobians = self.jacobian(element_id) + return _np.array( + [ + _np.linalg.det(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + + def compute_all_element_quad_points(self, recompute=False): + """Compute the quadrature points of all elements + + Parameters + ---------- + recompute: bool + Recompute quadrature points + """ + if self._all_element_quad_points is not None and not recompute: + return + + # compute element lengths and center points + element_lengths = _cartesian_product( + [_np.diff(dim_ukv) for dim_ukv in self._ukv] + ) + element_midpoints = ( + _cartesian_product([dim_ukv[:-1] for dim_ukv in self._ukv]) + + element_lengths / 2 + ) + # Scale quad points for each element + quad_points_centered = _np.einsum( + "ij,hj->hij", self._quad_positions / 2, element_lengths + ) + # apply offset to quad points + n_elements, n_quad_points, _ = quad_points_centered.shape + self._all_element_quad_points = quad_points_centered + _np.repeat( + element_midpoints.reshape(n_elements, 1, -1), n_quad_points, 1 + ) + + def compute_all_element_jacobians(self, recompute=False): + """Compute Jacobians of each element at each quadrature point + + Parameters + ---------- + recompute: bool + Recompute Jacobians + """ + if self._all_jacobians is not None and not recompute: + return + + self.compute_all_element_quad_points(recompute=recompute) + self._all_jacobians = _np.stack( + [ + self._spline.jacobian(quad_points) + for quad_points in self._all_element_quad_points + ] + ) + + def compute_all_element_jacobian_inverses(self, recompute=False): + """Compute Jacobians' inverses of each element at each quadrature point + + Parameters + ---------- + recompute: bool + Recompute Jacobians' inverses + """ + if self._all_jacobian_inverses is not None and not recompute: + return + + self.compute_all_element_jacobians(recompute=recompute) + + self._all_jacobian_inverses = _np.stack( + [ + _np.stack( + [ + _np.linalg.inv(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + for element_jacobians in self._all_jacobians + ] + ) + + def compute_all_element_jacobian_determinants(self, recompute=False): + """Compute Jacobians' determinants of each element at each quadrature point + + Parameters + ---------- + recompute: bool + Recompute Jacobians' determinants + """ + if self._all_jacobian_determinants is not None and not recompute: + return + + self.compute_all_element_jacobians(recompute=recompute) + + self._all_jacobian_determinants = _np.stack( + [ + _np.stack( + [ + _np.linalg.det(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + for element_jacobians in self._all_jacobians + ] + ) + + def _get_integral_measure(spline): """ Determines the appropriate measure to be used in integration @@ -359,6 +663,7 @@ class FieldIntegrator(_SplinepyBase): "_bezier_patches", "_global_positions", "_jacobians", + "_jacobian_inverses", "_jacobian_weights", ) @@ -484,12 +789,10 @@ def jacobian_weights(self): return self._jacobian_weights - @property def assemble_matrix(self, function, dim, matrix=None): """ """ pass - @property def assemble_vector(self, function, dim, matrix=None): """ """ pass From 042f1715a4ded25fd16e809940c8932a84ccdef1 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 1 Aug 2024 16:30:11 +0200 Subject: [PATCH 144/171] Add test for transformation class --- tests/helpme/test_integrate.py | 73 +++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 9dc287e63..1a1bcdbf6 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -42,9 +42,7 @@ def test_volume_integration_embedded(np_rng): # test for other types same spline assert np.allclose(bezier.bspline.integrate.volume(), expected_result) - assert np.allclose( - bezier.rationalbezier.integrate.volume(), expected_result - ) + assert np.allclose(bezier.rationalbezier.integrate.volume(), expected_result) assert np.allclose(bezier.nurbs.integrate.volume(), expected_result) # Check if equal after refinement @@ -155,18 +153,12 @@ def test_complex_geometry(np_rng): def test_assertions(np_rng): """Test the assertions in volume function""" - bezier = splinepy.Bezier( - degrees=[1, 2], control_points=np_rng.random((6, 1)) - ) + bezier = splinepy.Bezier(degrees=[1, 2], control_points=np_rng.random((6, 1))) - with pytest.raises( - ValueError, match=r"`Volume` not supported if para_dim > dim" - ): + with pytest.raises(ValueError, match=r"`Volume` not supported if para_dim > dim"): bezier.integrate.volume() - bezier = splinepy.Bezier( - degrees=[2, 2], control_points=np_rng.random((9, 2)) - ) + bezier = splinepy.Bezier(degrees=[2, 2], control_points=np_rng.random((9, 2))) with pytest.raises( ValueError, match=r"Integration order must be array of size para_dim" @@ -183,9 +175,7 @@ def volume_function(x): vf[:, 1] = col1_factor return vf - bezier = splinepy.Bezier( - degrees=[1, 2], control_points=np_rng.random((6, 2)) - ) + bezier = splinepy.Bezier(degrees=[1, 2], control_points=np_rng.random((6, 2))) assert np.allclose( [bezier.integrate.volume(), col1_factor * bezier.integrate.volume()], @@ -200,6 +190,55 @@ def volume_function(x): ) +def test_transformation_class(): + """Test element transformation of single patch""" + + # Create quadratic spline + spline = splinepy.BSpline( + degrees=[2, 2], + knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], + control_points=splinepy.utils.data.cartesian_product( + [np.linspace(0, 1, 3), np.linspace(0, 1, 3)] + ), + ) + + # Randomly insert knots along both parametric dimensions + spline.insert_knots(0, np.random.random(2)) + spline.insert_knots(1, np.random.random(2)) + + # Create transformation class for spline + trafo = splinepy.helpme.integrate.Transformation(spline) + + # Check whether element quadrature points lie inside element + ukvs = spline.unique_knots + trafo.compute_all_element_quad_points() + for element_id, quad_points in enumerate(trafo.all_quad_points): + grid_id = trafo.get_element_grid_id(element_id) + element_corners = [ + ukv[grid_dim_id : grid_dim_id + 2] + for ukv, grid_dim_id in zip(ukvs, grid_id) + ] + quad_points = trafo.all_quad_points[element_id] + # Check if quadrature points lie within element corners + for dim, corners in enumerate(element_corners): + assert np.all( + (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) + ) + + # For given spline, all Jacobians should be identity matrix + eye = np.eye(spline.para_dim) + trafo.compute_all_element_jacobian_inverses() + for element_jacobians in trafo.all_jacobians: + for jacobian_at_quad_point in element_jacobians: + assert np.allclose(eye, jacobian_at_quad_point) + + # For created spline, all determinants should equal one + trafo.compute_all_element_jacobian_determinants() + assert np.allclose( + trafo.all_jacobian_determinants, np.ones_like(trafo.all_jacobian_determinants) + ) + + def test_physical_function_integration(np_rng): # Analytical integral of y*(5-y) over [0,1]x[0,5] rectangle integral_analytical = 125 / 6 @@ -215,9 +254,7 @@ def parabolic_function(points): # Create rectangle domain xlin, ylin = np.linspace(0, 1, 3), np.linspace(0, 5, 3) - control_points = np.vstack( - [array.ravel() for array in np.meshgrid(xlin, ylin)] - ).T + control_points = np.vstack([array.ravel() for array in np.meshgrid(xlin, ylin)]).T rectangle = splinepy.BSpline( degrees=[2, 2], knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], From 0c56299400988c0bd027a768371efd6427e7a9dc Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 2 Aug 2024 10:47:31 +0200 Subject: [PATCH 145/171] Add custom and default quadrature order for transformation class --- splinepy/helpme/integrate.py | 61 ++++++++++++++++++++++++---------- tests/helpme/test_integrate.py | 4 +-- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 212a06f32..04ccd6b71 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -20,7 +20,7 @@ class Transformation: "_all_jacobian_determinants", ) - def __init__(self, spline): + def __init__(self, spline, orders=None): self._spline = spline self._para_dim = spline.para_dim if self._para_dim == 3: @@ -30,17 +30,26 @@ def __init__(self, spline): self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] # Gauss-Legendre quadrature points and weights - max_order = int(_np.max(self._spline.degrees)) - quad_positions, quad_weights = _np.polynomial.legendre.leggauss( - deg=max_order - ) - self._quad_positions = _cartesian_product( - [quad_positions for _ in range(self._para_dim)] - ) - self._quad_weights = _cartesian_product( - [quad_weights for _ in range(self._para_dim)] - ) - self._quad_weights = _np.prod(self._quad_weights, axis=1) + if orders is None: + quad_positions = [] + quad_weights = [] + for dim_quadrature_order in _default_quadrature_orders(spline): + quad_position, quad_weight = _np.polynomial.legendre.leggauss( + deg=dim_quadrature_order + ) + # Make quadrature points go from [0,1] instead of [-1,1] + quad_positions.append((quad_position + 1) / 2) + # Adjust weights accordingly + quad_weights.append(quad_weight / 2) + + self._quad_positions = _cartesian_product(quad_positions) + self._quad_weights = _np.prod( + _cartesian_product(quad_weights), axis=1 + ) + else: + self._quad_positions, self._quad_weights = ( + _get_quadrature_information(spline, orders) + ) self._all_element_quad_points = None self._all_jacobians = None @@ -232,12 +241,13 @@ def compute_all_element_quad_points(self, recompute=False): ) # Scale quad points for each element quad_points_centered = _np.einsum( - "ij,hj->hij", self._quad_positions / 2, element_lengths + "ij,hj->hij", self._quad_positions, element_lengths ) # apply offset to quad points n_elements, n_quad_points, _ = quad_points_centered.shape + offsets = element_midpoints - element_lengths / 2 self._all_element_quad_points = quad_points_centered + _np.repeat( - element_midpoints.reshape(n_elements, 1, -1), n_quad_points, 1 + (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 ) def compute_all_element_jacobians(self, recompute=False): @@ -671,7 +681,8 @@ def __init__(self, spline, orders=None): """ """ self._helpee = spline - self.reset(orders) + self._trafo = Transformation(spline, orders) + self.precompute_transformation() def reset(self, orders=None): """ """ @@ -690,6 +701,11 @@ def reset(self, orders=None): self._jacobians = None self._jacobian_weights = None + def precompute_transformation(self): + """Computes the quadrature points, jacobians and their determinants + of all elements in spline""" + self._trafo.compute_all_element_jacobian_determinants() + @property def positions(self): """ @@ -789,10 +805,19 @@ def jacobian_weights(self): return self._jacobian_weights - def assemble_matrix(self, function, dim, matrix=None): + def assemble_matrix(self, function, matrix=None): """ """ pass - def assemble_vector(self, function, dim, matrix=None): - """ """ + def assemble_vector(self, function, vector=None): + """Assemble the rhs for a given function + + Parameters + ------------ + function: callable + Function which defines how to assemble the rhs vector + vector: np.ndarray + Rhs vector. If None, a new vector will be created. Otherwise it will + add the corresponding to values to existing vector + """ pass diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 1a1bcdbf6..712ba92a2 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -218,7 +218,6 @@ def test_transformation_class(): ukv[grid_dim_id : grid_dim_id + 2] for ukv, grid_dim_id in zip(ukvs, grid_id) ] - quad_points = trafo.all_quad_points[element_id] # Check if quadrature points lie within element corners for dim, corners in enumerate(element_corners): assert np.all( @@ -235,7 +234,8 @@ def test_transformation_class(): # For created spline, all determinants should equal one trafo.compute_all_element_jacobian_determinants() assert np.allclose( - trafo.all_jacobian_determinants, np.ones_like(trafo.all_jacobian_determinants) + trafo.all_jacobian_determinants, + np.ones_like(trafo.all_jacobian_determinants), ) From 01c6744b384a446238bc56edead0b5273b31bdf7 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 2 Aug 2024 10:54:28 +0200 Subject: [PATCH 146/171] Restructure helpme/integrate.py --- splinepy/helpme/integrate.py | 939 +++++++++++++++++------------------ 1 file changed, 460 insertions(+), 479 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 04ccd6b71..3e5db35f6 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -6,320 +6,6 @@ from splinepy.utils.data import cartesian_product as _cartesian_product -class Transformation: - __slots__ = ( - "_spline", - "_para_dim", - "_ukv", - "_n_elems_per_dim", - "_quad_positions", - "_quad_weights", - "_all_element_quad_points", - "_all_jacobians", - "_all_jacobian_inverses", - "_all_jacobian_determinants", - ) - - def __init__(self, spline, orders=None): - self._spline = spline - self._para_dim = spline.para_dim - if self._para_dim == 3: - raise NotImplementedError("Not yet tested for 3D") - - self._ukv = self._spline.unique_knots - self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] - - # Gauss-Legendre quadrature points and weights - if orders is None: - quad_positions = [] - quad_weights = [] - for dim_quadrature_order in _default_quadrature_orders(spline): - quad_position, quad_weight = _np.polynomial.legendre.leggauss( - deg=dim_quadrature_order - ) - # Make quadrature points go from [0,1] instead of [-1,1] - quad_positions.append((quad_position + 1) / 2) - # Adjust weights accordingly - quad_weights.append(quad_weight / 2) - - self._quad_positions = _cartesian_product(quad_positions) - self._quad_weights = _np.prod( - _cartesian_product(quad_weights), axis=1 - ) - else: - self._quad_positions, self._quad_weights = ( - _get_quadrature_information(spline, orders) - ) - - self._all_element_quad_points = None - self._all_jacobians = None - self._all_jacobian_inverses = None - self._all_jacobian_determinants = None - - def check_element_id_validity(self, element_id): - """Check if given element ID is valid - - Parameters - ----------- - element_id: int - ID of element in spline's element. ID-array is 1D - """ - assert element_id >= 0 - assert element_id < _np.prod(self._n_elems_per_dim) - - @property - def all_quad_points(self): - """Quadrature points of all elements. - Dimensions [, , 2]""" - return self._all_element_quad_points - - @property - def all_jacobians(self): - """Jacobians of all elements. - Dimensions [ , , ]""" - return self._all_jacobians - - @property - def all_jacobian_inverses(self): - """Inverses of Jacobians of all elements. - Dimensions [ , , ]""" - return self._all_jacobian_inverses - - @property - def all_jacobian_determinants(self): - """Determinants of Jacobians of all elements. - Dimensions [ ]""" - return self._all_jacobian_determinants - - @property - def quadrature_weights(self): - return self._quad_weights - - def get_element_grid_id(self, element_id): - """Compute element ID in grid - - Parameters - ---------- - element_id: int - ID of element of spline - - Returns - --------- - element_grid_id: list - ID of element in grid - """ - if self._para_dim == 3: - raise NotImplementedError( - "Element grid ID not yet implemented for 3D" - ) - - n_elems_x = self._n_elems_per_dim[0] - grid_id = [element_id % n_elems_x, element_id // n_elems_x] - - return grid_id - - def get_element_quad_points(self, element_id): - """For given element computes quad points - - Parameters - ----------- - element_id: int - ID of element in spline's element - - Returns - ----------- - element_quad_points: np.ndarray - Quadrature points for element - """ - self.check_element_id_validity(element_id) - - if self._all_element_quad_points is not None: - return self._all_element_quad_points[element_id] - - element_grid_id = self.get_element_grid_id(element_id) - - element_corner_points = _np.vstack( - [ - ukv_dim[e_dim_id : (e_dim_id + 2)] - for ukv_dim, e_dim_id in zip(self._ukv, element_grid_id) - ] - ) - element_lengths = _np.diff(element_corner_points, axis=1).ravel() - element_midpoints = _np.mean(element_corner_points, axis=1) - - return self._quad_positions / 2 * element_lengths + element_midpoints - - def jacobian(self, element_id): - """Return Jacobian of single element at quadrature points - - Parameters - ------------ - element_id: list - ID of element in grid - - Returns - --------- - element_jacobian: np.ndarray - Jacobian of element evaluated at quadrature points - """ - if self._all_jacobians is not None: - return self._all_jacobians[element_id] - - element_quad_points = self.get_element_quad_points(element_id) - - return self._spline.jacobian(element_quad_points) - - def jacobian_inverse(self, element_id): - """Return inverse of Jacobian of single element, evaluated at quadrature points - - Parameters - ------------ - element_id: list - ID of element in grid - - Returns - --------- - element_inverse_jacobian: np.ndarray - Inverse of Jacobian of element evaluated at quadrature points - """ - if self._all_jacobian_inverses is not None: - return self._all_jacobian_inverses[element_id] - - element_jacobians = self.jacobian(element_id) - element_jacobian_inverse = _np.stack( - [ - _np.linalg.inv(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - - return element_jacobian_inverse - - def jacobian_determinant(self, element_id): - """Return determinant of Jacobian of single element, evaluated at - quadrature points - - Parameters - ------------ - element_id: list - ID of element in grid - - Returns - --------- - element_jacobian_determinant: np.ndarray - Determinant of Jacobian of element evaluated at quadrature points - """ - if self._all_jacobian_determinants is not None: - return self._all_jacobian_determinants[element_id] - - element_jacobians = self.jacobian(element_id) - return _np.array( - [ - _np.linalg.det(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - - def compute_all_element_quad_points(self, recompute=False): - """Compute the quadrature points of all elements - - Parameters - ---------- - recompute: bool - Recompute quadrature points - """ - if self._all_element_quad_points is not None and not recompute: - return - - # compute element lengths and center points - element_lengths = _cartesian_product( - [_np.diff(dim_ukv) for dim_ukv in self._ukv] - ) - element_midpoints = ( - _cartesian_product([dim_ukv[:-1] for dim_ukv in self._ukv]) - + element_lengths / 2 - ) - # Scale quad points for each element - quad_points_centered = _np.einsum( - "ij,hj->hij", self._quad_positions, element_lengths - ) - # apply offset to quad points - n_elements, n_quad_points, _ = quad_points_centered.shape - offsets = element_midpoints - element_lengths / 2 - self._all_element_quad_points = quad_points_centered + _np.repeat( - (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 - ) - - def compute_all_element_jacobians(self, recompute=False): - """Compute Jacobians of each element at each quadrature point - - Parameters - ---------- - recompute: bool - Recompute Jacobians - """ - if self._all_jacobians is not None and not recompute: - return - - self.compute_all_element_quad_points(recompute=recompute) - self._all_jacobians = _np.stack( - [ - self._spline.jacobian(quad_points) - for quad_points in self._all_element_quad_points - ] - ) - - def compute_all_element_jacobian_inverses(self, recompute=False): - """Compute Jacobians' inverses of each element at each quadrature point - - Parameters - ---------- - recompute: bool - Recompute Jacobians' inverses - """ - if self._all_jacobian_inverses is not None and not recompute: - return - - self.compute_all_element_jacobians(recompute=recompute) - - self._all_jacobian_inverses = _np.stack( - [ - _np.stack( - [ - _np.linalg.inv(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - for element_jacobians in self._all_jacobians - ] - ) - - def compute_all_element_jacobian_determinants(self, recompute=False): - """Compute Jacobians' determinants of each element at each quadrature point - - Parameters - ---------- - recompute: bool - Recompute Jacobians' determinants - """ - if self._all_jacobian_determinants is not None and not recompute: - return - - self.compute_all_element_jacobians(recompute=recompute) - - self._all_jacobian_determinants = _np.stack( - [ - _np.stack( - [ - _np.linalg.det(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - for element_jacobians in self._all_jacobians - ] - ) - - def _get_integral_measure(spline): """ Determines the appropriate measure to be used in integration @@ -435,25 +121,121 @@ def _get_quadrature_information(spline, orders=None): ) -def volume(spline, orders=None): - r"""Compute volume of a given spline +def volume(spline, orders=None): + r"""Compute volume of a given spline + + Parameters + ---------- + spline : Spline + (self if called via integrator) + splinepy - spline type + orders : array-like (optional) + order for gauss quadrature + + Returns + ------- + volume : float + Integral of dim-dimensional object + """ + from splinepy.spline import Spline as _Spline + + # Check i_nput type + if not isinstance(spline, _Spline): + raise NotImplementedError("integration only works for splines") + + # Retrieve aux info + meas = _get_integral_measure(spline) + positions, weights = _get_quadrature_information(spline, orders) + + # Calculate Volume + if spline.has_knot_vectors: + volume = _np.sum( + [_np.sum(meas(b, positions) * weights) for b in spline.extract.beziers()] + ) + else: + volume = _np.sum(meas(spline, positions) * weights) + return volume + + +def parametric_function( + spline, + function, + orders=None, +): + """Integrate a function defined within the parametric domain + + Parameters + ---------- + spline : Spline + (self if called via integrator) + function : Callable + orders : optional + + Returns + ------- + integral : np.ndarray + """ + from splinepy.spline import Spline as _Spline + + # Check i_nput type + if not isinstance(spline, _Spline): + raise NotImplementedError("integration only works for splines") + + # Retrieve aux info + meas = _get_integral_measure(spline) + positions, weights = _get_quadrature_information(spline, orders) + + # Calculate Volume + if spline.has_knot_vectors: + # positions must be mapped into each knot-element + para_view = spline.create.parametric_view(axes=False) + + # get initial shape + initial = function([positions[0]]) + result = _np.zeros(initial.shape[1]) + for bezier_element in para_view.extract.beziers(): + quad_positions = bezier_element.evaluate(positions) + result += _np.einsum( + "i...,i,i->...", + function(quad_positions), + meas(spline, quad_positions), + weights, + optimize=True, + ) + + else: + result = _np.sum( + function(positions) * meas(spline, positions) * weights, axis=1 + ) + return result + + +def _user_function( + spline, + function, + orders=None, + physical=False, +): + """Integrate a function defined within the selected domain Parameters ---------- spline : Spline (self if called via integrator) - splinepy - spline type - orders : array-like (optional) - order for gauss quadrature + function : Callable + orders : optional + physical : bool + If True, the function is defined in the physical domain. If False, + the function is defined in the parametric domain. + Default is False. Returns ------- - volume : float - Integral of dim-dimensional object + integral : np.ndarray """ from splinepy.spline import Spline as _Spline - # Check i_nput type + # Check input type if not isinstance(spline, _Spline): raise NotImplementedError("integration only works for splines") @@ -461,206 +243,404 @@ def volume(spline, orders=None): meas = _get_integral_measure(spline) positions, weights = _get_quadrature_information(spline, orders) + # define function to integrate + def _function(position): + if physical: + return function(spline.evaluate(position)) + else: + return function(position) + # Calculate Volume if spline.has_knot_vectors: - volume = _np.sum( - [_np.sum(meas(b, positions) * weights) for b in spline.extract.beziers()] - ) + # positions must be mapped into each knot-element + para_view = spline.create.parametric_view(axes=False) + + # get initial shape + initial = function([positions[0]]) + result = _np.zeros(initial.shape[1]) + for bezier_element in para_view.extract.beziers(): + # Get the bezier element scaling factor to get the correct + # element size from original knot element + knot_element_scaling_factor = _np.prod( + _np.diff(bezier_element.control_point_bounds, axis=0) + ) + quad_positions = bezier_element.evaluate(positions) + result += _np.einsum( + "i...,i,i->...", + _function(quad_positions), + meas(spline, quad_positions) * knot_element_scaling_factor, + weights, + optimize=True, + ) else: - volume = _np.sum(meas(spline, positions) * weights) - return volume + result = _np.einsum( + "i...,i,i->...", + _function(positions), + meas(spline, positions), + weights, + optimize=True, + ) + return result -def _user_function( +def physical_function( spline, function, orders=None, - physical=False, ): - """Integrate a function defined within the selected domain + """Integrate a function defined within the physical domain Parameters ---------- spline : Spline - (self if called via integrator) + The geometry over which the function is integrated function : Callable + The user-defined function to integrate. Can also be vector-valued orders : optional - physical : bool - If True, the function is defined in the physical domain. If False, - the function is defined in the parametric domain. - Default is False. + Quadrature order in parametric domain for numerical integration Returns ------- integral : np.ndarray + The computed integral. It is vector-valued if function is vector-valued """ - from splinepy.spline import Spline as _Spline + return _user_function(spline, function, orders=orders, physical=True) - # Check input type - if not isinstance(spline, _Spline): - raise NotImplementedError("integration only works for splines") - # Retrieve aux info - meas = _get_integral_measure(spline) - positions, weights = _get_quadrature_information(spline, orders) +class Integrator: + """Helper class to integrate some values on a given spline geometry + + Examples + -------- + + .. code-block:: python + + splinepy.helpme.integrate.volume() + # Equivalent to + spline.integrate.volume() + + Parameters + ---------- + spline : Spline + Spline parent + """ + + __slots__ = ("_helpee",) + + def __init__(self, spl): + self._helpee = spl + + @_wraps(volume) + def volume(self, *args, **kwargs): + return volume(self._helpee, *args, **kwargs) + + @_wraps(parametric_function) + def parametric_function(self, *args, **kwargs): + return parametric_function(self._helpee, *args, **kwargs) + + +class Transformation: + __slots__ = ( + "_spline", + "_para_dim", + "_ukv", + "_n_elems_per_dim", + "_quad_positions", + "_quad_weights", + "_all_element_quad_points", + "_all_jacobians", + "_all_jacobian_inverses", + "_all_jacobian_determinants", + ) + + def __init__(self, spline, orders=None): + self._spline = spline + self._para_dim = spline.para_dim + if self._para_dim == 3: + raise NotImplementedError("Not yet tested for 3D") + + self._ukv = self._spline.unique_knots + self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + + # Gauss-Legendre quadrature points and weights + if orders is None: + quad_positions = [] + quad_weights = [] + for dim_quadrature_order in _default_quadrature_orders(spline): + quad_position, quad_weight = _np.polynomial.legendre.leggauss( + deg=dim_quadrature_order + ) + # Make quadrature points go from [0,1] instead of [-1,1] + quad_positions.append((quad_position + 1) / 2) + # Adjust weights accordingly + quad_weights.append(quad_weight / 2) + + self._quad_positions = _cartesian_product(quad_positions) + self._quad_weights = _np.prod(_cartesian_product(quad_weights), axis=1) + else: + self._quad_positions, self._quad_weights = _get_quadrature_information( + spline, orders + ) + + self._all_element_quad_points = None + self._all_jacobians = None + self._all_jacobian_inverses = None + self._all_jacobian_determinants = None + + def check_element_id_validity(self, element_id): + """Check if given element ID is valid + + Parameters + ----------- + element_id: int + ID of element in spline's element. ID-array is 1D + """ + assert element_id >= 0 + assert element_id < _np.prod(self._n_elems_per_dim) + + @property + def all_quad_points(self): + """Quadrature points of all elements. + Dimensions [, , 2]""" + return self._all_element_quad_points + + @property + def all_jacobians(self): + """Jacobians of all elements. + Dimensions [ , , ]""" + return self._all_jacobians + + @property + def all_jacobian_inverses(self): + """Inverses of Jacobians of all elements. + Dimensions [ , , ]""" + return self._all_jacobian_inverses + + @property + def all_jacobian_determinants(self): + """Determinants of Jacobians of all elements. + Dimensions [ ]""" + return self._all_jacobian_determinants + + @property + def quadrature_weights(self): + return self._quad_weights + + def get_element_grid_id(self, element_id): + """Compute element ID in grid + + Parameters + ---------- + element_id: int + ID of element of spline + + Returns + --------- + element_grid_id: list + ID of element in grid + """ + if self._para_dim == 3: + raise NotImplementedError("Element grid ID not yet implemented for 3D") + + n_elems_x = self._n_elems_per_dim[0] + grid_id = [element_id % n_elems_x, element_id // n_elems_x] + + return grid_id + + def get_element_quad_points(self, element_id): + """For given element computes quad points + + Parameters + ----------- + element_id: int + ID of element in spline's element + + Returns + ----------- + element_quad_points: np.ndarray + Quadrature points for element + """ + self.check_element_id_validity(element_id) + + if self._all_element_quad_points is not None: + return self._all_element_quad_points[element_id] + + element_grid_id = self.get_element_grid_id(element_id) + + element_corner_points = _np.vstack( + [ + ukv_dim[e_dim_id : (e_dim_id + 2)] + for ukv_dim, e_dim_id in zip(self._ukv, element_grid_id) + ] + ) + element_lengths = _np.diff(element_corner_points, axis=1).ravel() + element_midpoints = _np.mean(element_corner_points, axis=1) + + return self._quad_positions / 2 * element_lengths + element_midpoints + + def jacobian(self, element_id): + """Return Jacobian of single element at quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid - # define function to integrate - def _function(position): - if physical: - return function(spline.evaluate(position)) - else: - return function(position) + Returns + --------- + element_jacobian: np.ndarray + Jacobian of element evaluated at quadrature points + """ + if self._all_jacobians is not None: + return self._all_jacobians[element_id] - # Calculate Volume - if spline.has_knot_vectors: - # positions must be mapped into each knot-element - para_view = spline.create.parametric_view(axes=False) + element_quad_points = self.get_element_quad_points(element_id) - # get initial shape - initial = function([positions[0]]) - result = _np.zeros(initial.shape[1]) - for bezier_element in para_view.extract.beziers(): - # Get the bezier element scaling factor to get the correct - # element size from original knot element - knot_element_scaling_factor = _np.prod( - _np.diff(bezier_element.control_point_bounds, axis=0) - ) - quad_positions = bezier_element.evaluate(positions) - result += _np.einsum( - "i...,i,i->...", - _function(quad_positions), - meas(spline, quad_positions) * knot_element_scaling_factor, - weights, - optimize=True, - ) - else: - result = _np.einsum( - "i...,i,i->...", - _function(positions), - meas(spline, positions), - weights, - optimize=True, - ) - return result + return self._spline.jacobian(element_quad_points) + def jacobian_inverse(self, element_id): + """Return inverse of Jacobian of single element, evaluated at quadrature points -def parametric_function( - spline, - function, - orders=None, -): - """Integrate a function defined within the parametric domain + Parameters + ------------ + element_id: list + ID of element in grid - Parameters - ---------- - spline : Spline - The geometry over which the function is integrated - function : Callable - The user-defined function to integrate. It takes points in the parametric - dimension as input and outputs a scalar or an array of scalars. - orders : optional - Quadrature orders for numerical integration + Returns + --------- + element_inverse_jacobian: np.ndarray + Inverse of Jacobian of element evaluated at quadrature points + """ + if self._all_jacobian_inverses is not None: + return self._all_jacobian_inverses[element_id] - Returns - ------- - integral : np.ndarray - """ - from splinepy.spline import Spline as _Spline + element_jacobians = self.jacobian(element_id) + element_jacobian_inverse = _np.stack( + [_np.linalg.inv(element_jacobian) for element_jacobian in element_jacobians] + ) - # Check input type - if not isinstance(spline, _Spline): - raise NotImplementedError("integration only works for splines") + return element_jacobian_inverse - # Retrieve aux info - meas = _get_integral_measure(spline) - positions, weights = _get_quadrature_information(spline, orders) + def jacobian_determinant(self, element_id): + """Return determinant of Jacobian of single element, evaluated at + quadrature points - # Calculate Volume - if spline.has_knot_vectors: - # positions must be mapped into each knot-element - para_view = spline.create.parametric_view(axes=False) + Parameters + ------------ + element_id: list + ID of element in grid - # get initial shape - initial = function([positions[0]]) - result = _np.zeros(initial.shape[1]) - for bezier_element in para_view.extract.beziers(): - quad_positions = bezier_element.evaluate(positions) - result += _np.einsum( - "i...,i,i->...", - function(quad_positions), - meas(spline, quad_positions), - weights, - optimize=True, - ) + Returns + --------- + element_jacobian_determinant: np.ndarray + Determinant of Jacobian of element evaluated at quadrature points + """ + if self._all_jacobian_determinants is not None: + return self._all_jacobian_determinants[element_id] - else: - result = _np.einsum( - "i...,i,i->...", - function(positions), - meas(spline, positions), - weights, + element_jacobians = self.jacobian(element_id) + return _np.array( + [_np.linalg.det(element_jacobian) for element_jacobian in element_jacobians] ) - return result - -def physical_function( - spline, - function, - orders=None, -): - """Integrate a function defined within the physical domain + def compute_all_element_quad_points(self, recompute=False): + """Compute the quadrature points of all elements - Parameters - ---------- - spline : Spline - The geometry over which the function is integrated - function : Callable - The user-defined function to integrate. Can also be vector-valued - orders : optional - Quadrature order in parametric domain for numerical integration + Parameters + ---------- + recompute: bool + Recompute quadrature points + """ + if self._all_element_quad_points is not None and not recompute: + return - Returns - ------- - integral : np.ndarray - The computed integral. It is vector-valued if function is vector-valued - """ - return _user_function(spline, function, orders=orders, physical=True) + # compute element lengths and center points + element_lengths = _cartesian_product( + [_np.diff(dim_ukv) for dim_ukv in self._ukv] + ) + element_midpoints = ( + _cartesian_product([dim_ukv[:-1] for dim_ukv in self._ukv]) + + element_lengths / 2 + ) + # Scale quad points for each element + quad_points_centered = _np.einsum( + "ij,hj->hij", self._quad_positions, element_lengths + ) + # apply offset to quad points + n_elements, n_quad_points, _ = quad_points_centered.shape + offsets = element_midpoints - element_lengths / 2 + self._all_element_quad_points = quad_points_centered + _np.repeat( + (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 + ) + def compute_all_element_jacobians(self, recompute=False): + """Compute Jacobians of each element at each quadrature point -class Integrator: - """Helper class to integrate some values on a given spline geometry + Parameters + ---------- + recompute: bool + Recompute Jacobians + """ + if self._all_jacobians is not None and not recompute: + return - Examples - -------- + self.compute_all_element_quad_points(recompute=recompute) + self._all_jacobians = _np.stack( + [ + self._spline.jacobian(quad_points) + for quad_points in self._all_element_quad_points + ] + ) - .. code-block:: python + def compute_all_element_jacobian_inverses(self, recompute=False): + """Compute Jacobians' inverses of each element at each quadrature point - splinepy.helpme.integrate.volume() - # Equivalent to - spline.integrate.volume() + Parameters + ---------- + recompute: bool + Recompute Jacobians' inverses + """ + if self._all_jacobian_inverses is not None and not recompute: + return - Parameters - ---------- - spline : Spline - Spline parent - """ + self.compute_all_element_jacobians(recompute=recompute) - __slots__ = ("_helpee",) + self._all_jacobian_inverses = _np.stack( + [ + _np.stack( + [ + _np.linalg.inv(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + for element_jacobians in self._all_jacobians + ] + ) - def __init__(self, spl): - self._helpee = spl + def compute_all_element_jacobian_determinants(self, recompute=False): + """Compute Jacobians' determinants of each element at each quadrature point - @_wraps(volume) - def volume(self, *args, **kwargs): - return volume(self._helpee, *args, **kwargs) + Parameters + ---------- + recompute: bool + Recompute Jacobians' determinants + """ + if self._all_jacobian_determinants is not None and not recompute: + return - @_wraps(parametric_function) - def parametric_function(self, *args, **kwargs): - return parametric_function(self._helpee, *args, **kwargs) + self.compute_all_element_jacobians(recompute=recompute) - @_wraps(physical_function) - def physical_function(self, *args, **kwargs): - return physical_function(self._helpee, *args, **kwargs) + self._all_jacobian_determinants = _np.stack( + [ + _np.stack( + [ + _np.linalg.det(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + for element_jacobians in self._all_jacobians + ] + ) class FieldIntegrator(_SplinepyBase): @@ -684,6 +664,9 @@ def __init__(self, spline, orders=None): self._trafo = Transformation(spline, orders) self.precompute_transformation() + self._rhs = None + self._system_matrix = None + def reset(self, orders=None): """ """ if orders is None: @@ -805,19 +788,17 @@ def jacobian_weights(self): return self._jacobian_weights - def assemble_matrix(self, function, matrix=None): + def assemble_matrix(self, function): """ """ pass - def assemble_vector(self, function, vector=None): - """Assemble the rhs for a given function + def assemble_vector(self, function): + """Assemble the rhs for a given function. If rhs is already assembled, + it will add values on top of existing rhs. Parameters ------------ function: callable Function which defines how to assemble the rhs vector - vector: np.ndarray - Rhs vector. If None, a new vector will be created. Otherwise it will - add the corresponding to values to existing vector """ pass From 6bccfdfc0e946985be77acc436b38d2ee09479c0 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 5 Aug 2024 14:26:39 +0200 Subject: [PATCH 147/171] Add assembly to field integrator --- splinepy/helpme/integrate.py | 365 ++++++++++++++++++++++------------- 1 file changed, 231 insertions(+), 134 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 3e5db35f6..7a6b31752 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -4,6 +4,11 @@ from splinepy._base import SplinepyBase as _SplinepyBase from splinepy.utils.data import cartesian_product as _cartesian_product +from splinepy.utils.data import has_scipy as _has_scipy + +if _has_scipy: + from scipy.sparse import dok_matrix as _dok_matrix + from scipy.sparse.linalg import spsolve as _spsolve def _get_integral_measure(spline): @@ -342,31 +347,47 @@ def parametric_function(self, *args, **kwargs): class Transformation: __slots__ = ( "_spline", + "_solution_field", + "_mapper", "_para_dim", "_ukv", - "_n_elems_per_dim", + "_n_elems", "_quad_positions", "_quad_weights", + "_grid_ids", + "_all_supports", "_all_element_quad_points", "_all_jacobians", "_all_jacobian_inverses", "_all_jacobian_determinants", ) - def __init__(self, spline, orders=None): + def __init__(self, spline, solution_field=None, orders=None): self._spline = spline + self._solution_field = solution_field + if solution_field is not None: + self._mapper = self._solution_field.mapper(reference=self._spline) + self._para_dim = spline.para_dim if self._para_dim == 3: raise NotImplementedError("Not yet tested for 3D") - self._ukv = self._spline.unique_knots - self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + if solution_field is None: + self._ukv = spline.unique_knots + else: + self._ukv = self._solution_field.unique_knots + n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + self._n_elems = _np.prod(n_elems_per_dim) # Gauss-Legendre quadrature points and weights + spline_for_quad = spline if solution_field is None else solution_field + if orders is None: quad_positions = [] quad_weights = [] - for dim_quadrature_order in _default_quadrature_orders(spline): + for dim_quadrature_order in _default_quadrature_orders( + spline_for_quad + ): quad_position, quad_weight = _np.polynomial.legendre.leggauss( deg=dim_quadrature_order ) @@ -378,10 +399,17 @@ def __init__(self, spline, orders=None): self._quad_positions = _cartesian_product(quad_positions) self._quad_weights = _np.prod(_cartesian_product(quad_weights), axis=1) else: - self._quad_positions, self._quad_weights = _get_quadrature_information( - spline, orders + self._quad_positions, self._quad_weights = ( + _get_quadrature_information(spline_for_quad, orders) ) + # Precompute grid IDs + self._grid_ids = _cartesian_product( + [_np.arange(n_elems) for n_elems in n_elems_per_dim], + reverse=True, + ) + + self._all_supports = None self._all_element_quad_points = None self._all_jacobians = None self._all_jacobian_inverses = None @@ -396,7 +424,13 @@ def check_element_id_validity(self, element_id): ID of element in spline's element. ID-array is 1D """ assert element_id >= 0 - assert element_id < _np.prod(self._n_elems_per_dim) + assert element_id < self._n_elems + + @property + def all_supports(self): + """Supports of all quadrature points. + List of entries of support""" + return self._all_supports @property def all_quad_points(self): @@ -442,10 +476,7 @@ def get_element_grid_id(self, element_id): if self._para_dim == 3: raise NotImplementedError("Element grid ID not yet implemented for 3D") - n_elems_x = self._n_elems_per_dim[0] - grid_id = [element_id % n_elems_x, element_id // n_elems_x] - - return grid_id + return self._grid_ids[element_id, :] def get_element_quad_points(self, element_id): """For given element computes quad points @@ -476,7 +507,36 @@ def get_element_quad_points(self, element_id): element_lengths = _np.diff(element_corner_points, axis=1).ravel() element_midpoints = _np.mean(element_corner_points, axis=1) - return self._quad_positions / 2 * element_lengths + element_midpoints + # Bring center to origin and scale + element_quad_points = (self._quad_positions - 0.5) * element_lengths + # Apply offset + element_quad_points += element_midpoints + + return element_quad_points + + def get_element_support(self, element_id): + """Get support for quadrature points in element + + Parameters + ------------ + element_id: int + ID of element + + Returns + --------- + support: np.ndarray + Support for element. All quadrature points have same support + """ + element_quad_points = self.get_element_quad_points(element_id) + + # All quad points in element have same support, therefore take arbitrary + # one + relevant_quad_point = element_quad_points[0, :] + + if self._solution_field is None: + return self._spline.support(relevant_quad_point) + else: + return self._solution_field.support(relevant_quad_point) def jacobian(self, element_id): """Return Jacobian of single element at quadrature points @@ -573,6 +633,28 @@ def compute_all_element_quad_points(self, recompute=False): (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 ) + def compute_all_supports(self, recompute=False): + """Compute the support for all quadrature points + + Parameters + -------------- + recompute: bool + Recompute quadrature points + """ + if self._all_supports is not None and not recompute: + return + + self.compute_all_element_quad_points(recompute=recompute) + relevant_spline = ( + self._spline + if self._solution_field is None + else self._solution_field + ) + self._all_supports = [ + relevant_spline.support(quad_points[:1, :]).ravel() + for quad_points in self._all_element_quad_points + ] + def compute_all_element_jacobians(self, recompute=False): """Compute Jacobians of each element at each quadrature point @@ -645,160 +727,175 @@ def compute_all_element_jacobian_determinants(self, recompute=False): class FieldIntegrator(_SplinepyBase): __slots__ = ( - "_positions", - "_weights", "_helpee", - "_orders", + "_solution_field", + "_mapper", + "_global_rhs", + "_global_system_matrix", + "_trafo", "_supports", - "_bezier_patches", - "_global_positions", - "_jacobians", - "_jacobian_inverses", - "_jacobian_weights", + "_ndofs", ) - def __init__(self, spline, orders=None): + def __init__(self, geometry, solution_field=None, orders=None): """ """ - self._helpee = spline - - self._trafo = Transformation(spline, orders) - self.precompute_transformation() + self._helpee = geometry + if solution_field is None: + self._solution_field = geometry.copy() + self._solution_field.control_points = _np.ones( + (geometry.cps.shape[0], 1) + ) + else: + self._solution_field = solution_field + self._ndofs = int( + _np.prod( + [ + len(kv) - 1 - deg + for deg, kv in zip( + self._solution_field.degrees, + self._solution_field.knot_vectors, + ) + ] + ) + ) + self._mapper = self._solution_field.mapper(reference=self._helpee) - self._rhs = None - self._system_matrix = None + self.reset(orders) def reset(self, orders=None): """ """ - if orders is None: - self._orders = _default_quadrature_orders(self._helpee) - else: - self._orders = orders - - self._positions, self._weights = _get_quadrature_information( - self._helpee, self._orders + self._trafo = Transformation( + self._helpee, self._solution_field, orders ) + self.precompute_transformation() - self._bezier_patches = None - self._global_positions = None self._supports = None - self._jacobians = None - self._jacobian_weights = None + self._global_rhs = None + self._global_system_matrix = None def precompute_transformation(self): """Computes the quadrature points, jacobians and their determinants of all elements in spline""" + self._trafo.compute_all_supports() self._trafo.compute_all_element_jacobian_determinants() - @property - def positions(self): - """ - Normalized Quadrature positions. Can use this value for - """ - return self._positions - - @property - def global_positions(self): - """ - Quadrature points in global position - """ - if self._global_positions is not None: - return self._global_positions - - # TODO: clamped knot vector check once it's merged - lower_bounds_per_dim = [] - span_scales_per_dim = [] - for ukv in self._helpee.unique_knots: - lower_bounds_per_dim.append(ukv[:-1]) - span_scales_per_dim.append(_np.diff(ukv)) - lower_bounds = _cartesian_product(lower_bounds_per_dim, reverse=True) - span_scales = _cartesian_product(span_scales_per_dim, reverse=True) - - # add lower bound as offsets using np.broadcast rules - n_quads, dim = self.positions.shape - n_elem = len(lower_bounds) - - # create normalized quad points for each element - self._global_positions = _np.tile(self.positions, (n_elem, 1)).reshape( - n_elem, n_quads, dim - ) - # scale them - self._global_positions *= span_scales.reshape(n_elem, 1, dim) - # apply offset - self._global_positions += lower_bounds.reshape(n_elem, 1, dim) - - return self._global_positions - @property def supports(self): """ """ - if self._supports is not None: - return self._supports + if self._supports is None: + self._supports = self._helpee.supports(self._trafo.all_quad_points) - self._supports = self._helpee.supports(self.global_positions) return self._supports - @property - def quadrature_weights(self): - """ """ - return self._weights + def assemble_matrix(self, function): + """Assemble the system matrix for a given function. If system matrix is + already assembled, it will add values on top of existing matrix. - @property - def bezier_patches(self): - """Returns extracted bezier patches. - These beziers can be used to compute jacobians and weights thereof. + Parameters + ------------ + function: callable + Function which defines how to assemble the system matrix """ - if self._bezier_patches is not None: - return self._bezier_patches - - self._bezier_patches = self._helpee.extract.beziers() - return self._bezier_patches - - @property - def jacobians(self): - if self._jacobians is not None: - return self._jacobians - - self._jacobians = [bp.jacobian(self.positions) for bp in self.bezier_patches] + # Initialize system matrix if not already + if self._global_system_matrix is None: + global_size = (self._ndofs, self._ndofs) + if _has_scipy: + self._global_system_matrix = _dok_matrix(global_size) + else: + self._global_system_matrix = _np.zeros(global_size) + + quad_weights = self._trafo.quadrature_weights + + # Element loop + for element_jacobian_det, element_support, element_quad_points in zip( + self._trafo.all_jacobian_determinants, + self._trafo.all_supports, + self._trafo.all_quad_points, + ): + element_matrix = function( + self._mapper, + quad_points=element_quad_points, + quad_weights=quad_weights, + jacobian_det=element_jacobian_det, + ) + matrix_element_support = _cartesian_product( + [element_support, element_support] + ) + self._global_system_matrix[ + matrix_element_support[:, 0], matrix_element_support[:, 1] + ] += element_matrix - return self._jacobians + def assemble_vector(self, function, current_sol=None): + """Assemble the rhs for a given function. If rhs is already assembled, + it will add values on top of existing rhs. - @property - def jacobian_inverses(self): - if self._jacobian_inverses is not None: - return self._jacobian_inverses + Parameters + ------------ + function: callable + Function which defines how to assemble the rhs vector + current_sol: np.ndarray + Current solution vector. Needed for nonlinear forms + """ + # Initialize rhs vector + if self._global_rhs is None: + self._global_rhs = _np.zeros(self._ndofs) + + quad_weights = self._trafo.quadrature_weights + + # Element loop + for element_jacobian_det, element_support, element_quad_points in zip( + self._trafo.all_jacobian_determinants, + self._trafo.all_supports, + self._trafo.all_quad_points, + ): + element_vector = function( + self._solution_field, + quad_points=element_quad_points, + quad_weights=quad_weights, + jacobian_det=element_jacobian_det, + current_sol=current_sol, + ) + self._global_rhs[element_support] += element_vector - # will raise if this isn't square - self._jacobian_inverses = [_np.linalg.inv(j) for j in self.jacobians] + def check_if_assembled(self): + if self._global_system_matrix is None or self._global_rhs is None: + raise ValueError("System is not yet fully assembled") - return self._jacobian_inverses + def assign_homogeneous_dirichlet_boundary_conditions(self): + self.check_if_assembled() - @property - def jacobian_weights(self): - """ - Jacobian determinants if jacobian is a square matrix. - Otherwise, - """ - if self._jacobian_weights is not None: - return self._jacobian_weights + relevant_spline = ( + self._helpee + if self._solution_field is None + else self._solution_field + ) - meas = _get_integral_measure(self._helpee) - self._jacobian_weights = [ - meas(bp, self.positions) for bp in self.bezier_patches - ] + multi_index = relevant_spline.multi_index - return self._jacobian_weights + indices = _np.unique( + _np.hstack( + ( + multi_index[0, :], + multi_index[-1, :], + multi_index[:, 0], + multi_index[:, -1], + ) + ) + ) - def assemble_matrix(self, function): - """ """ - pass + self._global_system_matrix[indices, :] = 0 + self._global_system_matrix[indices, indices] = 1 + self._global_rhs[indices] = 0 - def assemble_vector(self, function): - """Assemble the rhs for a given function. If rhs is already assembled, - it will add values on top of existing rhs. + def solve_linear_system(self): + self.check_if_assembled() - Parameters - ------------ - function: callable - Function which defines how to assemble the rhs vector - """ - pass + if _has_scipy: + solution_vector = _spsolve( + self._global_system_matrix.tocsr(), self._global_rhs + ) + else: + solution_vector = _np.linalg.solve( + self._global_system_matrix, self._global_rhs + ) + self._solution_field.control_points = solution_vector.reshape(-1, 1) From b803e612ae096c6eb5ca13b02d9be28aa76177f7 Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 5 Aug 2024 14:37:01 +0200 Subject: [PATCH 148/171] Add Laplace FieldIntegrator example --- ...lerkin_laplace_problem_field_integrator.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 examples/iga/galerkin_laplace_problem_field_integrator.py diff --git a/examples/iga/galerkin_laplace_problem_field_integrator.py b/examples/iga/galerkin_laplace_problem_field_integrator.py new file mode 100644 index 000000000..7178cb1fa --- /dev/null +++ b/examples/iga/galerkin_laplace_problem_field_integrator.py @@ -0,0 +1,87 @@ +r""" +This is a showcase of splinepy's FieldIntegrator class and is doing the same as +'examples/galerkin_laplace_problem.py'. +""" + +import numpy as np + +import splinepy as sp + +# Test Case +n_refine = 15 + +def prepare_geometry_and_solution_field(): + # Define the Geometry + geometry = sp.BSpline( + degrees=[2, 2], + control_points=[ + [0.0, 0.0], + [1.0, 0.5], + [2.0, 0.2], + [0.5, 1.5], + [1.0, 1.5], + [1.5, 1.5], + [0.0, 3.0], + [1.0, 2.5], + [2.0, 3.0], + ], + knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], + ) + + # Define the solution field + solution_field = sp.BSpline( + degrees=geometry.degrees, + control_points=np.ones((geometry.control_points.shape[0], 1)), + knot_vectors=geometry.knot_vectors, + ) + + # Use refinement + solution_field.elevate_degrees([0, 1, 0, 1]) + new_knots = np.linspace(1 / n_refine, 1, n_refine, endpoint=False) + solution_field.insert_knots(0, new_knots) + solution_field.insert_knots(1, new_knots) + + return geometry, solution_field + +def poisson_lhs(mapper, quad_points, quad_weights, jacobian_det): + bf_gradient, _ = mapper.basis_gradient_and_support(quad_points) + element_matrix = np.einsum( + "qid,qjd,q,q->ij", + bf_gradient, + bf_gradient, + quad_weights, + jacobian_det, + optimize=True, + ) + return element_matrix.ravel() + +def poisson_rhs(solution_field, quad_points, quad_weights, jacobian_det, current_sol=None): + bf, _ = solution_field.basis_and_support(quad_points) + element_vector = np.einsum( + "qj,q,q->j", + bf, + quad_weights, + jacobian_det, + optimize=True + ) + return element_vector + +if __name__ == "__main__": + geometry, solution_field = prepare_geometry_and_solution_field() + + fi = sp.helpme.integrate.FieldIntegrator(geometry=geometry, solution_field=solution_field) + + fi.assemble_matrix(poisson_lhs) + fi.assemble_vector(poisson_rhs) + fi.assign_homogeneous_dirichlet_boundary_conditions() + + fi.solve_linear_system() + + # Plot geometry and field + geometry.spline_data["field"] = solution_field + geometry.show_options["data"] = "field" + geometry.show_options["cmap"] = "jet" + geometry.show_options["lighting"] = "off" + geometry.show_options["scalarbar"] = True + geometry.show(knots=True, control_points=False) + \ No newline at end of file From 302588dd69b434fee9df335d7af40e52ce6276ad Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 5 Aug 2024 19:04:59 +0200 Subject: [PATCH 149/171] Add inhomogeneous Dirichlet BCs from function --- ...lerkin_laplace_problem_field_integrator.py | 77 +++++--- splinepy/helpme/integrate.py | 165 ++++++++++++++++-- tests/helpme/test_integrate.py | 12 +- 3 files changed, 206 insertions(+), 48 deletions(-) diff --git a/examples/iga/galerkin_laplace_problem_field_integrator.py b/examples/iga/galerkin_laplace_problem_field_integrator.py index 7178cb1fa..d3e505eef 100644 --- a/examples/iga/galerkin_laplace_problem_field_integrator.py +++ b/examples/iga/galerkin_laplace_problem_field_integrator.py @@ -1,6 +1,12 @@ r""" -This is a showcase of splinepy's FieldIntegrator class and is doing the same as -'examples/galerkin_laplace_problem.py'. +This is a showcase of splinepy's FieldIntegrator class. It solves the Poisson +equation in the form +.. math:: + -\Delta u = f +with source term f=1. + +First, homogeneous Dirichlet boundary conditions are applied. Then, inhomogeneous ones +are applied. """ import numpy as np @@ -10,6 +16,7 @@ # Test Case n_refine = 15 + def prepare_geometry_and_solution_field(): # Define the Geometry geometry = sp.BSpline( @@ -27,22 +34,22 @@ def prepare_geometry_and_solution_field(): ], knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], ) - + # Define the solution field - solution_field = sp.BSpline( - degrees=geometry.degrees, - control_points=np.ones((geometry.control_points.shape[0], 1)), - knot_vectors=geometry.knot_vectors, + solution_field = geometry.copy() + solution_field.control_points = np.ones( + (geometry.control_points.shape[0], 1) ) - + # Use refinement solution_field.elevate_degrees([0, 1, 0, 1]) new_knots = np.linspace(1 / n_refine, 1, n_refine, endpoint=False) solution_field.insert_knots(0, new_knots) solution_field.insert_knots(1, new_knots) - + return geometry, solution_field + def poisson_lhs(mapper, quad_points, quad_weights, jacobian_det): bf_gradient, _ = mapper.basis_gradient_and_support(quad_points) element_matrix = np.einsum( @@ -55,33 +62,47 @@ def poisson_lhs(mapper, quad_points, quad_weights, jacobian_det): ) return element_matrix.ravel() -def poisson_rhs(solution_field, quad_points, quad_weights, jacobian_det, current_sol=None): - bf, _ = solution_field.basis_and_support(quad_points) + +def poisson_rhs(mapper, quad_points, quad_weights, jacobian_det): + bf = mapper._field_reference.basis(quad_points) element_vector = np.einsum( - "qj,q,q->j", - bf, - quad_weights, - jacobian_det, - optimize=True + "qj,q,q->j", bf, quad_weights, jacobian_det, optimize=True ) return element_vector + +def show_solution(geometry, solution_field): + geometry.spline_data["field"] = solution_field + geometry.show_options["data"] = "field" + geometry.show_options["cmap"] = "jet" + geometry.show_options["lighting"] = "off" + geometry.show_options["scalarbar"] = True + geometry.show(knots=True, control_points=False) + + if __name__ == "__main__": geometry, solution_field = prepare_geometry_and_solution_field() - - fi = sp.helpme.integrate.FieldIntegrator(geometry=geometry, solution_field=solution_field) - + + fi = sp.helpme.integrate.FieldIntegrator( + geometry=geometry, solution_field=solution_field + ) + fi.assemble_matrix(poisson_lhs) fi.assemble_vector(poisson_rhs) + + # Homogeneous Dirichlet boundary conditions fi.assign_homogeneous_dirichlet_boundary_conditions() - fi.solve_linear_system() - # Plot geometry and field - geometry.spline_data["field"] = solution_field - geometry.show_options["data"] = "field" - geometry.show_options["cmap"] = "jet" - geometry.show_options["lighting"] = "off" - geometry.show_options["scalarbar"] = True - geometry.show(knots=True, control_points=False) - \ No newline at end of file + show_solution(geometry, solution_field) + + # Inhomogeneous Dirichlet boundary conditions + def dirichlet_function(points): + """ + On the boundary apply: g(x,y) = x/10 + """ + return points[:, 0] / 10 + + fi.apply_dirichlet_boundary_conditions(dirichlet_function) + fi.solve_linear_system() + show_solution(geometry, solution_field) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 7a6b31752..f70467105 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -787,7 +787,7 @@ def supports(self): return self._supports - def assemble_matrix(self, function): + def assemble_matrix(self, function, matrixout=None): """Assemble the system matrix for a given function. If system matrix is already assembled, it will add values on top of existing matrix. @@ -795,15 +795,31 @@ def assemble_matrix(self, function): ------------ function: callable Function which defines how to assemble the system matrix + matrixout: np.ndarray + Assembled matrix will be stored there. Default is global system matrix """ # Initialize system matrix if not already - if self._global_system_matrix is None: + if self._global_system_matrix is None and matrixout is None: global_size = (self._ndofs, self._ndofs) if _has_scipy: self._global_system_matrix = _dok_matrix(global_size) else: self._global_system_matrix = _np.zeros(global_size) + # If other matrix is used, check if it compatible + if matrixout is not None: + if _has_scipy: + assert isinstance( + matrixout, _dok_matrix + ), "Matrixout must be scipy sparse dok matrix" + else: + assert isinstance(matrixout, _np.ndarray) + assert matrixout.shape == (self._ndofs, self._ndofs) + + system_matrix = ( + self._global_system_matrix if matrixout is None else matrixout + ) + quad_weights = self._trafo.quadrature_weights # Element loop @@ -813,7 +829,7 @@ def assemble_matrix(self, function): self._trafo.all_quad_points, ): element_matrix = function( - self._mapper, + mapper=self._mapper, quad_points=element_quad_points, quad_weights=quad_weights, jacobian_det=element_jacobian_det, @@ -821,11 +837,11 @@ def assemble_matrix(self, function): matrix_element_support = _cartesian_product( [element_support, element_support] ) - self._global_system_matrix[ + system_matrix[ matrix_element_support[:, 0], matrix_element_support[:, 1] ] += element_matrix - def assemble_vector(self, function, current_sol=None): + def assemble_vector(self, function, current_sol=None, vectorout=None): """Assemble the rhs for a given function. If rhs is already assembled, it will add values on top of existing rhs. @@ -840,7 +856,21 @@ def assemble_vector(self, function, current_sol=None): if self._global_rhs is None: self._global_rhs = _np.zeros(self._ndofs) - quad_weights = self._trafo.quadrature_weights + # Ensure that current solution has right dimensions + if current_sol is not None: + assert len(current_sol) == self._ndofs + + if vectorout is not None: + assert isinstance(vectorout, _np.ndarray) + assert len(vectorout) == self._ndofs + + rhs_vector = self._global_rhs if vectorout is None else vectorout + + # Prepare function arguments, which stay the same for all elements + function_args = { + "mapper": self._mapper, + "quad_weights": self._trafo.quadrature_weights, + } # Element loop for element_jacobian_det, element_support, element_quad_points in zip( @@ -848,22 +878,32 @@ def assemble_vector(self, function, current_sol=None): self._trafo.all_supports, self._trafo.all_quad_points, ): - element_vector = function( - self._solution_field, - quad_points=element_quad_points, - quad_weights=quad_weights, - jacobian_det=element_jacobian_det, - current_sol=current_sol, - ) - self._global_rhs[element_support] += element_vector + # Assemble element vector + function_args["quad_points"] = element_quad_points + function_args["jacobian_det"] = element_jacobian_det + if current_sol is not None: + function_args["current_sol"] = current_sol[element_support] + element_vector = function(**function_args) + + # Add element vector to global rhs vector + rhs_vector[element_support] += element_vector def check_if_assembled(self): + """ + Check if system matrix and rhs are already assembled + """ if self._global_system_matrix is None or self._global_rhs is None: raise ValueError("System is not yet fully assembled") - def assign_homogeneous_dirichlet_boundary_conditions(self): - self.check_if_assembled() + def get_boundary_dofs(self): + """ + Get indices of boundary dofs. + Returns + ------------- + indices: np.ndarray + Indices of relevant boundary dofs + """ relevant_spline = ( self._helpee if self._solution_field is None @@ -883,11 +923,104 @@ def assign_homogeneous_dirichlet_boundary_conditions(self): ) ) + return indices + + def assign_homogeneous_dirichlet_boundary_conditions(self): + """ + Assembles homogeneous Dirichlet boundary conditions + """ + self.check_if_assembled() + + indices = self.get_boundary_dofs() + self._global_system_matrix[indices, :] = 0 self._global_system_matrix[indices, indices] = 1 self._global_rhs[indices] = 0 + def apply_dirichlet_boundary_conditions(self, function): + """ + Applies Dirichlet boundary conditions via :math:`L^2`-projection. + + For a given function g, it solves the following equation + + .. math:: \\sum\\limits_{j=1}^n \alpha_j (N_i, N_j) = (g, N_i) \\quad + \forall i = 1, \\dots, n. + + Then, the :math:`Pg`, the :math:`L^2`-projection of the function, is + given by + + .. math:: Pg(x) = \\sum\\limits_{j=1}^n \alpha_j \\phi_j(x) + + For the Dirichlet boundary conditions, only the DoFs corresponding to + the boundaries are taken from the :math:`L^2`-projection. + + Parameters + ------------- + function: callable + Function to apply. Input are points, output is scalar + """ + self.check_if_assembled() + + # Assemble mass matrix + global_size = (self._ndofs, self._ndofs) + mass_matrix = ( + _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) + ) + + def mass_lhs(mapper, quad_points, quad_weights, jacobian_det): + bf_values = mapper._field_reference.basis(quad_points) + element_matrix = _np.einsum( + "qi,qj,q,q->ij", + bf_values, + bf_values, + quad_weights, + jacobian_det, + optimize=True, + ) + return element_matrix.ravel() + + self.assemble_matrix(mass_lhs, matrixout=mass_matrix) + + # Assemble rhs: f(x) * N + rhs_vector = _np.zeros(self._ndofs) + + def rhs_function(mapper, quad_points, quad_weights, jacobian_det): + bf = mapper._field_reference.basis(quad_points) + quad_points_forward = mapper._geometry_reference.evaluate( + quad_points + ) + function_values = function(quad_points_forward) + element_vector = _np.einsum( + "qj,q,q,q->j", + bf, + function_values, + quad_weights, + jacobian_det, + optimize=True, + ) + return element_vector + + self.assemble_vector(rhs_function, vectorout=rhs_vector) + + # Solve system to get all dofs + dof_vector = _np.empty(self._ndofs) + if _has_scipy: + dof_vector = _spsolve(mass_matrix.tocsr(), rhs_vector) + else: + dof_vector = _np.linalg.solve(mass_matrix, rhs_vector) + + # Get relevant dofs + indices = self.get_boundary_dofs() + + # Apply Dirichlet to relevant boundary dofs + self._global_system_matrix[indices, :] = 0 + self._global_system_matrix[indices, indices] = 1 + self._global_rhs[indices] = dof_vector[indices] + def solve_linear_system(self): + """ + Solve linear system for system matrix and rhs + """ self.check_if_assembled() if _has_scipy: diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 712ba92a2..08b1ffdc4 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -212,31 +212,35 @@ def test_transformation_class(): # Check whether element quadrature points lie inside element ukvs = spline.unique_knots trafo.compute_all_element_quad_points() + # Check quadrature points of each element for element_id, quad_points in enumerate(trafo.all_quad_points): grid_id = trafo.get_element_grid_id(element_id) + # Extract the corners of the current element element_corners = [ ukv[grid_dim_id : grid_dim_id + 2] for ukv, grid_dim_id in zip(ukvs, grid_id) ] - # Check if quadrature points lie within element corners + # Check if quadrature points lie within element for dim, corners in enumerate(element_corners): assert np.all( (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) - ) + ), f"Quadrature points do not lie within element for dimension {dim}" # For given spline, all Jacobians should be identity matrix eye = np.eye(spline.para_dim) trafo.compute_all_element_jacobian_inverses() for element_jacobians in trafo.all_jacobians: for jacobian_at_quad_point in element_jacobians: - assert np.allclose(eye, jacobian_at_quad_point) + assert np.allclose(eye, jacobian_at_quad_point), ( + "All Jacobians should be identity matrix" + ) # For created spline, all determinants should equal one trafo.compute_all_element_jacobian_determinants() assert np.allclose( trafo.all_jacobian_determinants, np.ones_like(trafo.all_jacobian_determinants), - ) + ), "All Jacobians' determinants should be equal to one" def test_physical_function_integration(np_rng): From 9408b4bb1c2fe7bffcac2856a0d4bbc56f492002 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 6 Aug 2024 09:39:48 +0200 Subject: [PATCH 150/171] Add system integrator --- splinepy/helpme/integrate.py | 94 ++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index f70467105..64c449cc1 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -794,8 +794,8 @@ def assemble_matrix(self, function, matrixout=None): Parameters ------------ function: callable - Function which defines how to assemble the system matrix - matrixout: np.ndarray + Function which defines how to assemble an element matrix + matrixout: np.ndarray / scipy.sparse matrix Assembled matrix will be stored there. Default is global system matrix """ # Initialize system matrix if not already @@ -848,9 +848,11 @@ def assemble_vector(self, function, current_sol=None, vectorout=None): Parameters ------------ function: callable - Function which defines how to assemble the rhs vector + Function which defines how to assemble an element vector current_sol: np.ndarray Current solution vector. Needed for nonlinear forms + vectorout: np.ndarray + Assembled rhs vector will be stored there. Default is global rhs """ # Initialize rhs vector if self._global_rhs is None: @@ -888,6 +890,92 @@ def assemble_vector(self, function, current_sol=None, vectorout=None): # Add element vector to global rhs vector rhs_vector[element_support] += element_vector + def assemble_matrix_and_vector( + self, function, current_sol=None, matrixout=None, vectorout=None + ): + """Assemble the system matrix and rhs vector for a given function. If system + matrix is already assembled, it will add values on top of existing matrix. + The same goes for the rhs vector + + Parameters + ------------ + function: callable + Function which defines how to assemble an element matrix and vector + current_sol: np.ndarray + Current solution vector. Needed for nonlinear forms + matrixout: np.ndarray / scipy.sparse matrix + Assembled matrix will be stored there. Default is global system matrix + vectorout: np.ndarray + Assembled rhs vector will be stored there. Default is global rhs + """ + # Initialize system matrix if not already + if self._global_system_matrix is None and matrixout is None: + global_size = (self._ndofs, self._ndofs) + if _has_scipy: + self._global_system_matrix = _dok_matrix(global_size) + else: + self._global_system_matrix = _np.zeros(global_size) + + # If other matrix is used, check if it compatible + if matrixout is not None: + if _has_scipy: + assert isinstance( + matrixout, _dok_matrix + ), "Matrixout must be scipy sparse dok matrix" + else: + assert isinstance(matrixout, _np.ndarray) + assert matrixout.shape == (self._ndofs, self._ndofs) + + # Set matrix accordingly + system_matrix = ( + self._global_system_matrix if matrixout is None else matrixout + ) + + # Initialize rhs vector + if self._global_rhs is None: + self._global_rhs = _np.zeros(self._ndofs) + + # Ensure that current solution has right dimensions + if current_sol is not None: + assert len(current_sol) == self._ndofs + + if vectorout is not None: + assert isinstance(vectorout, _np.ndarray) + assert len(vectorout) == self._ndofs + + # Set rhs vector accordingly + rhs_vector = self._global_rhs if vectorout is None else vectorout + + # Prepare function arguments, which stay the same for all elements + function_args = { + "mapper": self._mapper, + "quad_weights": self._trafo.quadrature_weights, + } + + # Element loop + for element_jacobian_det, element_support, element_quad_points in zip( + self._trafo.all_jacobian_determinants, + self._trafo.all_supports, + self._trafo.all_quad_points, + ): + # Assemble element matrix and vector + function_args["quad_points"] = element_quad_points + function_args["jacobian_det"] = element_jacobian_det + if current_sol is not None: + function_args["current_sol"] = current_sol[element_support] + element_matrix, element_vector = function(**function_args) + + # Add element vector to global system matrix + matrix_element_support = _cartesian_product( + [element_support, element_support] + ) + system_matrix[ + matrix_element_support[:, 0], matrix_element_support[:, 1] + ] += element_matrix + + # Add element vector to global rhs vector + rhs_vector[element_support] += element_vector + def check_if_assembled(self): """ Check if system matrix and rhs are already assembled From 617b19b72f04e87a7f597280ee1a22905f5060de Mon Sep 17 00:00:00 2001 From: Jaewook Lee Date: Tue, 13 Feb 2024 12:29:09 +0100 Subject: [PATCH 151/171] Rebase from main --- docs/source/handle_markdown.py | 18 ++++++++++++++++++ splinepy/helpme/integrate.py | 8 ++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/source/handle_markdown.py b/docs/source/handle_markdown.py index c76566de2..213aaa3c3 100644 --- a/docs/source/handle_markdown.py +++ b/docs/source/handle_markdown.py @@ -208,6 +208,24 @@ def process_file( line = line.replace(item[1], str(new_path)) # noqa: PLW2901 content += f"{line}" + # super special links (just special images) that the sphinx markdown + # parser won't correctly handle since they are in html tags. + special_links = get_special_links(content) + for item in special_links: + if not item[0].strip(): + warnings.warn( + f"Empty link in `{file}`. Link name `{item[1]}` link path " + f"`{item[0]}`. Will ignore link.", + stacklevel=3, + ) + continue + if item[0].startswith("http"): # skip http links and anchors + continue + else: + # just link to static folder in docs + new_path = "_static/" + str(pathlib.Path(item[1]).name) + content = content.replace(item[1], str(new_path)) + os.chdir(original_cwd) if return_content: diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 64c449cc1..7960656ba 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -209,8 +209,12 @@ def parametric_function( ) else: - result = _np.sum( - function(positions) * meas(spline, positions) * weights, axis=1 + result = _np.einsum( + "i...,i,i->...", + function(positions), + meas(spline, positions), + weights, + optimize=True, ) return result From 004cf98680a6e40a95bbec97758df2a498b8347c Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 1 Aug 2024 16:30:11 +0200 Subject: [PATCH 152/171] Merge test integrate --- tests/helpme/test_integrate.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 08b1ffdc4..96faad59e 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -181,7 +181,6 @@ def volume_function(x): [bezier.integrate.volume(), col1_factor * bezier.integrate.volume()], bezier.integrate.parametric_function(volume_function), ) - # try bsplines bspline = bezier.bspline assert np.allclose( @@ -192,7 +191,6 @@ def volume_function(x): def test_transformation_class(): """Test element transformation of single patch""" - # Create quadratic spline spline = splinepy.BSpline( degrees=[2, 2], @@ -212,35 +210,31 @@ def test_transformation_class(): # Check whether element quadrature points lie inside element ukvs = spline.unique_knots trafo.compute_all_element_quad_points() - # Check quadrature points of each element for element_id, quad_points in enumerate(trafo.all_quad_points): grid_id = trafo.get_element_grid_id(element_id) - # Extract the corners of the current element element_corners = [ ukv[grid_dim_id : grid_dim_id + 2] for ukv, grid_dim_id in zip(ukvs, grid_id) ] - # Check if quadrature points lie within element + quad_points = trafo.all_quad_points[element_id] + # Check if quadrature points lie within element corners for dim, corners in enumerate(element_corners): assert np.all( (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) - ), f"Quadrature points do not lie within element for dimension {dim}" + ) # For given spline, all Jacobians should be identity matrix eye = np.eye(spline.para_dim) trafo.compute_all_element_jacobian_inverses() for element_jacobians in trafo.all_jacobians: for jacobian_at_quad_point in element_jacobians: - assert np.allclose(eye, jacobian_at_quad_point), ( - "All Jacobians should be identity matrix" - ) + assert np.allclose(eye, jacobian_at_quad_point) # For created spline, all determinants should equal one trafo.compute_all_element_jacobian_determinants() assert np.allclose( - trafo.all_jacobian_determinants, - np.ones_like(trafo.all_jacobian_determinants), - ), "All Jacobians' determinants should be equal to one" + trafo.all_jacobian_determinants, np.ones_like(trafo.all_jacobian_determinants) + ) def test_physical_function_integration(np_rng): From 9fff528835c608da4798cc2a1fa7818d520ad952 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 2 Aug 2024 10:47:31 +0200 Subject: [PATCH 153/171] Add custom and default quadrature order for transformation class --- splinepy/helpme/integrate.py | 355 ++++++++++++++++++++++++++++++++- tests/helpme/test_integrate.py | 5 +- 2 files changed, 356 insertions(+), 4 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 7960656ba..bd967547f 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -10,6 +10,319 @@ from scipy.sparse import dok_matrix as _dok_matrix from scipy.sparse.linalg import spsolve as _spsolve +class Transformation: + __slots__ = ( + "_spline", + "_para_dim", + "_ukv", + "_n_elems_per_dim", + "_quad_positions", + "_quad_weights", + "_all_element_quad_points", + "_all_jacobians", + "_all_jacobian_inverses", + "_all_jacobian_determinants", + ) + + def __init__(self, spline, orders=None): + self._spline = spline + self._para_dim = spline.para_dim + if self._para_dim == 3: + raise NotImplementedError("Not yet tested for 3D") + + self._ukv = self._spline.unique_knots + self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + + # Gauss-Legendre quadrature points and weights + if orders is None: + quad_positions = [] + quad_weights = [] + for dim_quadrature_order in _default_quadrature_orders(spline): + quad_position, quad_weight = _np.polynomial.legendre.leggauss( + deg=dim_quadrature_order + ) + # Make quadrature points go from [0,1] instead of [-1,1] + quad_positions.append((quad_position + 1) / 2) + # Adjust weights accordingly + quad_weights.append(quad_weight / 2) + + self._quad_positions = _cartesian_product(quad_positions) + self._quad_weights = _np.prod( + _cartesian_product(quad_weights), axis=1 + ) + else: + self._quad_positions, self._quad_weights = ( + _get_quadrature_information(spline, orders) + ) + + self._all_element_quad_points = None + self._all_jacobians = None + self._all_jacobian_inverses = None + self._all_jacobian_determinants = None + + def check_element_id_validity(self, element_id): + """Check if given element ID is valid + + Parameters + ----------- + element_id: int + ID of element in spline's element. ID-array is 1D + """ + assert element_id >= 0 + assert element_id < _np.prod(self._n_elems_per_dim) + + @property + def all_quad_points(self): + """Quadrature points of all elements. + Dimensions [, , 2]""" + return self._all_element_quad_points + + @property + def all_jacobians(self): + """Jacobians of all elements. + Dimensions [ , , ]""" + return self._all_jacobians + + @property + def all_jacobian_inverses(self): + """Inverses of Jacobians of all elements. + Dimensions [ , , ]""" + return self._all_jacobian_inverses + + @property + def all_jacobian_determinants(self): + """Determinants of Jacobians of all elements. + Dimensions [ ]""" + return self._all_jacobian_determinants + + @property + def quadrature_weights(self): + return self._quad_weights + + def get_element_grid_id(self, element_id): + """Compute element ID in grid + + Parameters + ---------- + element_id: int + ID of element of spline + + Returns + --------- + element_grid_id: list + ID of element in grid + """ + if self._para_dim == 3: + raise NotImplementedError( + "Element grid ID not yet implemented for 3D" + ) + + n_elems_x = self._n_elems_per_dim[0] + grid_id = [element_id % n_elems_x, element_id // n_elems_x] + + return grid_id + + def get_element_quad_points(self, element_id): + """For given element computes quad points + + Parameters + ----------- + element_id: int + ID of element in spline's element + + Returns + ----------- + element_quad_points: np.ndarray + Quadrature points for element + """ + self.check_element_id_validity(element_id) + + if self._all_element_quad_points is not None: + return self._all_element_quad_points[element_id] + + element_grid_id = self.get_element_grid_id(element_id) + + element_corner_points = _np.vstack( + [ + ukv_dim[e_dim_id : (e_dim_id + 2)] + for ukv_dim, e_dim_id in zip(self._ukv, element_grid_id) + ] + ) + element_lengths = _np.diff(element_corner_points, axis=1).ravel() + element_midpoints = _np.mean(element_corner_points, axis=1) + + return self._quad_positions / 2 * element_lengths + element_midpoints + + def jacobian(self, element_id): + """Return Jacobian of single element at quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid + + Returns + --------- + element_jacobian: np.ndarray + Jacobian of element evaluated at quadrature points + """ + if self._all_jacobians is not None: + return self._all_jacobians[element_id] + + element_quad_points = self.get_element_quad_points(element_id) + + return self._spline.jacobian(element_quad_points) + + def jacobian_inverse(self, element_id): + """Return inverse of Jacobian of single element, evaluated at quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid + + Returns + --------- + element_inverse_jacobian: np.ndarray + Inverse of Jacobian of element evaluated at quadrature points + """ + if self._all_jacobian_inverses is not None: + return self._all_jacobian_inverses[element_id] + + element_jacobians = self.jacobian(element_id) + element_jacobian_inverse = _np.stack( + [ + _np.linalg.inv(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + + return element_jacobian_inverse + + def jacobian_determinant(self, element_id): + """Return determinant of Jacobian of single element, evaluated at + quadrature points + + Parameters + ------------ + element_id: list + ID of element in grid + + Returns + --------- + element_jacobian_determinant: np.ndarray + Determinant of Jacobian of element evaluated at quadrature points + """ + if self._all_jacobian_determinants is not None: + return self._all_jacobian_determinants[element_id] + + element_jacobians = self.jacobian(element_id) + return _np.array( + [ + _np.linalg.det(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + + def compute_all_element_quad_points(self, recompute=False): + """Compute the quadrature points of all elements + + Parameters + ---------- + recompute: bool + Recompute quadrature points + """ + if self._all_element_quad_points is not None and not recompute: + return + + # compute element lengths and center points + element_lengths = _cartesian_product( + [_np.diff(dim_ukv) for dim_ukv in self._ukv] + ) + element_midpoints = ( + _cartesian_product([dim_ukv[:-1] for dim_ukv in self._ukv]) + + element_lengths / 2 + ) + # Scale quad points for each element + quad_points_centered = _np.einsum( + "ij,hj->hij", self._quad_positions, element_lengths + ) + # apply offset to quad points + n_elements, n_quad_points, _ = quad_points_centered.shape + offsets = element_midpoints - element_lengths / 2 + self._all_element_quad_points = quad_points_centered + _np.repeat( + (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 + ) + + def compute_all_element_jacobians(self, recompute=False): + """Compute Jacobians of each element at each quadrature point + + Parameters + ---------- + recompute: bool + Recompute Jacobians + """ + if self._all_jacobians is not None and not recompute: + return + + self.compute_all_element_quad_points(recompute=recompute) + self._all_jacobians = _np.stack( + [ + self._spline.jacobian(quad_points) + for quad_points in self._all_element_quad_points + ] + ) + + def compute_all_element_jacobian_inverses(self, recompute=False): + """Compute Jacobians' inverses of each element at each quadrature point + + Parameters + ---------- + recompute: bool + Recompute Jacobians' inverses + """ + if self._all_jacobian_inverses is not None and not recompute: + return + + self.compute_all_element_jacobians(recompute=recompute) + + self._all_jacobian_inverses = _np.stack( + [ + _np.stack( + [ + _np.linalg.inv(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + for element_jacobians in self._all_jacobians + ] + ) + + def compute_all_element_jacobian_determinants(self, recompute=False): + """Compute Jacobians' determinants of each element at each quadrature point + + Parameters + ---------- + recompute: bool + Recompute Jacobians' determinants + """ + if self._all_jacobian_determinants is not None and not recompute: + return + + self.compute_all_element_jacobians(recompute=recompute) + + self._all_jacobian_determinants = _np.stack( + [ + _np.stack( + [ + _np.linalg.det(element_jacobian) + for element_jacobian in element_jacobians + ] + ) + for element_jacobians in self._all_jacobians + ] + ) + def _get_integral_measure(spline): """ @@ -764,7 +1077,8 @@ def __init__(self, geometry, solution_field=None, orders=None): ) self._mapper = self._solution_field.mapper(reference=self._helpee) - self.reset(orders) + self._trafo = Transformation(spline, orders) + self.precompute_transformation() def reset(self, orders=None): """ """ @@ -783,6 +1097,45 @@ def precompute_transformation(self): self._trafo.compute_all_supports() self._trafo.compute_all_element_jacobian_determinants() + @property + def positions(self): + """ + Normalized Quadrature positions. Can use this value for + """ + return self._positions + + @property + def global_positions(self): + """ + Quadrature points in global position + """ + if self._global_positions is not None: + return self._global_positions + + # TODO: clamped knot vector check once it's merged + lower_bounds_per_dim = [] + span_scales_per_dim = [] + for ukv in self._helpee.unique_knots: + lower_bounds_per_dim.append(ukv[:-1]) + span_scales_per_dim.append(_np.diff(ukv)) + lower_bounds = _cartesian_product(lower_bounds_per_dim, reverse=True) + span_scales = _cartesian_product(span_scales_per_dim, reverse=True) + + # add lower bound as offsets using np.broadcast rules + n_quads, dim = self.positions.shape + n_elem = len(lower_bounds) + + # create normalized quad points for each element + self._global_positions = _np.tile(self.positions, (n_elem, 1)).reshape( + n_elem, n_quads, dim + ) + # scale them + self._global_positions *= span_scales.reshape(n_elem, 1, dim) + # apply offset + self._global_positions += lower_bounds.reshape(n_elem, 1, dim) + + return self._global_positions + @property def supports(self): """ """ diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 96faad59e..c329537a7 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -1,5 +1,4 @@ import numpy as np -import pytest import splinepy @@ -216,7 +215,6 @@ def test_transformation_class(): ukv[grid_dim_id : grid_dim_id + 2] for ukv, grid_dim_id in zip(ukvs, grid_id) ] - quad_points = trafo.all_quad_points[element_id] # Check if quadrature points lie within element corners for dim, corners in enumerate(element_corners): assert np.all( @@ -233,7 +231,8 @@ def test_transformation_class(): # For created spline, all determinants should equal one trafo.compute_all_element_jacobian_determinants() assert np.allclose( - trafo.all_jacobian_determinants, np.ones_like(trafo.all_jacobian_determinants) + trafo.all_jacobian_determinants, + np.ones_like(trafo.all_jacobian_determinants), ) From 2974a10053ef85d0699d94087f635d707371cc97 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 2 Aug 2024 10:54:28 +0200 Subject: [PATCH 154/171] Restrucutre helpme/integrate.py --- splinepy/helpme/integrate.py | 569 +++------------------------------ tests/helpme/test_integrate.py | 1 + 2 files changed, 40 insertions(+), 530 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index bd967547f..cc6048720 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -10,319 +10,6 @@ from scipy.sparse import dok_matrix as _dok_matrix from scipy.sparse.linalg import spsolve as _spsolve -class Transformation: - __slots__ = ( - "_spline", - "_para_dim", - "_ukv", - "_n_elems_per_dim", - "_quad_positions", - "_quad_weights", - "_all_element_quad_points", - "_all_jacobians", - "_all_jacobian_inverses", - "_all_jacobian_determinants", - ) - - def __init__(self, spline, orders=None): - self._spline = spline - self._para_dim = spline.para_dim - if self._para_dim == 3: - raise NotImplementedError("Not yet tested for 3D") - - self._ukv = self._spline.unique_knots - self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] - - # Gauss-Legendre quadrature points and weights - if orders is None: - quad_positions = [] - quad_weights = [] - for dim_quadrature_order in _default_quadrature_orders(spline): - quad_position, quad_weight = _np.polynomial.legendre.leggauss( - deg=dim_quadrature_order - ) - # Make quadrature points go from [0,1] instead of [-1,1] - quad_positions.append((quad_position + 1) / 2) - # Adjust weights accordingly - quad_weights.append(quad_weight / 2) - - self._quad_positions = _cartesian_product(quad_positions) - self._quad_weights = _np.prod( - _cartesian_product(quad_weights), axis=1 - ) - else: - self._quad_positions, self._quad_weights = ( - _get_quadrature_information(spline, orders) - ) - - self._all_element_quad_points = None - self._all_jacobians = None - self._all_jacobian_inverses = None - self._all_jacobian_determinants = None - - def check_element_id_validity(self, element_id): - """Check if given element ID is valid - - Parameters - ----------- - element_id: int - ID of element in spline's element. ID-array is 1D - """ - assert element_id >= 0 - assert element_id < _np.prod(self._n_elems_per_dim) - - @property - def all_quad_points(self): - """Quadrature points of all elements. - Dimensions [, , 2]""" - return self._all_element_quad_points - - @property - def all_jacobians(self): - """Jacobians of all elements. - Dimensions [ , , ]""" - return self._all_jacobians - - @property - def all_jacobian_inverses(self): - """Inverses of Jacobians of all elements. - Dimensions [ , , ]""" - return self._all_jacobian_inverses - - @property - def all_jacobian_determinants(self): - """Determinants of Jacobians of all elements. - Dimensions [ ]""" - return self._all_jacobian_determinants - - @property - def quadrature_weights(self): - return self._quad_weights - - def get_element_grid_id(self, element_id): - """Compute element ID in grid - - Parameters - ---------- - element_id: int - ID of element of spline - - Returns - --------- - element_grid_id: list - ID of element in grid - """ - if self._para_dim == 3: - raise NotImplementedError( - "Element grid ID not yet implemented for 3D" - ) - - n_elems_x = self._n_elems_per_dim[0] - grid_id = [element_id % n_elems_x, element_id // n_elems_x] - - return grid_id - - def get_element_quad_points(self, element_id): - """For given element computes quad points - - Parameters - ----------- - element_id: int - ID of element in spline's element - - Returns - ----------- - element_quad_points: np.ndarray - Quadrature points for element - """ - self.check_element_id_validity(element_id) - - if self._all_element_quad_points is not None: - return self._all_element_quad_points[element_id] - - element_grid_id = self.get_element_grid_id(element_id) - - element_corner_points = _np.vstack( - [ - ukv_dim[e_dim_id : (e_dim_id + 2)] - for ukv_dim, e_dim_id in zip(self._ukv, element_grid_id) - ] - ) - element_lengths = _np.diff(element_corner_points, axis=1).ravel() - element_midpoints = _np.mean(element_corner_points, axis=1) - - return self._quad_positions / 2 * element_lengths + element_midpoints - - def jacobian(self, element_id): - """Return Jacobian of single element at quadrature points - - Parameters - ------------ - element_id: list - ID of element in grid - - Returns - --------- - element_jacobian: np.ndarray - Jacobian of element evaluated at quadrature points - """ - if self._all_jacobians is not None: - return self._all_jacobians[element_id] - - element_quad_points = self.get_element_quad_points(element_id) - - return self._spline.jacobian(element_quad_points) - - def jacobian_inverse(self, element_id): - """Return inverse of Jacobian of single element, evaluated at quadrature points - - Parameters - ------------ - element_id: list - ID of element in grid - - Returns - --------- - element_inverse_jacobian: np.ndarray - Inverse of Jacobian of element evaluated at quadrature points - """ - if self._all_jacobian_inverses is not None: - return self._all_jacobian_inverses[element_id] - - element_jacobians = self.jacobian(element_id) - element_jacobian_inverse = _np.stack( - [ - _np.linalg.inv(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - - return element_jacobian_inverse - - def jacobian_determinant(self, element_id): - """Return determinant of Jacobian of single element, evaluated at - quadrature points - - Parameters - ------------ - element_id: list - ID of element in grid - - Returns - --------- - element_jacobian_determinant: np.ndarray - Determinant of Jacobian of element evaluated at quadrature points - """ - if self._all_jacobian_determinants is not None: - return self._all_jacobian_determinants[element_id] - - element_jacobians = self.jacobian(element_id) - return _np.array( - [ - _np.linalg.det(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - - def compute_all_element_quad_points(self, recompute=False): - """Compute the quadrature points of all elements - - Parameters - ---------- - recompute: bool - Recompute quadrature points - """ - if self._all_element_quad_points is not None and not recompute: - return - - # compute element lengths and center points - element_lengths = _cartesian_product( - [_np.diff(dim_ukv) for dim_ukv in self._ukv] - ) - element_midpoints = ( - _cartesian_product([dim_ukv[:-1] for dim_ukv in self._ukv]) - + element_lengths / 2 - ) - # Scale quad points for each element - quad_points_centered = _np.einsum( - "ij,hj->hij", self._quad_positions, element_lengths - ) - # apply offset to quad points - n_elements, n_quad_points, _ = quad_points_centered.shape - offsets = element_midpoints - element_lengths / 2 - self._all_element_quad_points = quad_points_centered + _np.repeat( - (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 - ) - - def compute_all_element_jacobians(self, recompute=False): - """Compute Jacobians of each element at each quadrature point - - Parameters - ---------- - recompute: bool - Recompute Jacobians - """ - if self._all_jacobians is not None and not recompute: - return - - self.compute_all_element_quad_points(recompute=recompute) - self._all_jacobians = _np.stack( - [ - self._spline.jacobian(quad_points) - for quad_points in self._all_element_quad_points - ] - ) - - def compute_all_element_jacobian_inverses(self, recompute=False): - """Compute Jacobians' inverses of each element at each quadrature point - - Parameters - ---------- - recompute: bool - Recompute Jacobians' inverses - """ - if self._all_jacobian_inverses is not None and not recompute: - return - - self.compute_all_element_jacobians(recompute=recompute) - - self._all_jacobian_inverses = _np.stack( - [ - _np.stack( - [ - _np.linalg.inv(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - for element_jacobians in self._all_jacobians - ] - ) - - def compute_all_element_jacobian_determinants(self, recompute=False): - """Compute Jacobians' determinants of each element at each quadrature point - - Parameters - ---------- - recompute: bool - Recompute Jacobians' determinants - """ - if self._all_jacobian_determinants is not None and not recompute: - return - - self.compute_all_element_jacobians(recompute=recompute) - - self._all_jacobian_determinants = _np.stack( - [ - _np.stack( - [ - _np.linalg.det(element_jacobian) - for element_jacobian in element_jacobians - ] - ) - for element_jacobians in self._all_jacobians - ] - ) - def _get_integral_measure(spline): """ @@ -522,111 +209,21 @@ def parametric_function( ) else: - result = _np.einsum( - "i...,i,i->...", - function(positions), - meas(spline, positions), - weights, - optimize=True, - ) - return result - - -def _user_function( - spline, - function, - orders=None, - physical=False, -): - """Integrate a function defined within the selected domain - - Parameters - ---------- - spline : Spline - (self if called via integrator) - function : Callable - orders : optional - physical : bool - If True, the function is defined in the physical domain. If False, - the function is defined in the parametric domain. - Default is False. - - Returns - ------- - integral : np.ndarray - """ - from splinepy.spline import Spline as _Spline - - # Check input type - if not isinstance(spline, _Spline): - raise NotImplementedError("integration only works for splines") - - # Retrieve aux info - meas = _get_integral_measure(spline) - positions, weights = _get_quadrature_information(spline, orders) - - # define function to integrate - def _function(position): - if physical: - return function(spline.evaluate(position)) - else: - return function(position) - - # Calculate Volume - if spline.has_knot_vectors: - # positions must be mapped into each knot-element - para_view = spline.create.parametric_view(axes=False) - - # get initial shape - initial = function([positions[0]]) - result = _np.zeros(initial.shape[1]) - for bezier_element in para_view.extract.beziers(): - # Get the bezier element scaling factor to get the correct - # element size from original knot element - knot_element_scaling_factor = _np.prod( - _np.diff(bezier_element.control_point_bounds, axis=0) - ) - quad_positions = bezier_element.evaluate(positions) - result += _np.einsum( - "i...,i,i->...", - _function(quad_positions), - meas(spline, quad_positions) * knot_element_scaling_factor, - weights, - optimize=True, - ) - else: - result = _np.einsum( - "i...,i,i->...", - _function(positions), - meas(spline, positions), - weights, - optimize=True, + result = _np.sum( + function(positions) * meas(spline, positions) * weights, axis=1 ) return result def physical_function( - spline, - function, - orders=None, + function, # noqa ARG001 + orders, # noqa ARG001 ): - """Integrate a function defined within the physical domain - - Parameters - ---------- - spline : Spline - The geometry over which the function is integrated - function : Callable - The user-defined function to integrate. Can also be vector-valued - orders : optional - Quadrature order in parametric domain for numerical integration - - Returns - ------- - integral : np.ndarray - The computed integral. It is vector-valued if function is vector-valued - """ - return _user_function(spline, function, orders=orders, physical=True) + """Integrate a function defined within the physical domain""" + raise NotImplementedError( + "Function not implemented yet. Please feel free to write an issue, if " + "you need it: github.com/tatarata/splinepy/issues" + ) class Integrator: @@ -664,47 +261,31 @@ def parametric_function(self, *args, **kwargs): class Transformation: __slots__ = ( "_spline", - "_solution_field", - "_mapper", "_para_dim", "_ukv", - "_n_elems", + "_n_elems_per_dim", "_quad_positions", "_quad_weights", - "_grid_ids", - "_all_supports", "_all_element_quad_points", "_all_jacobians", "_all_jacobian_inverses", "_all_jacobian_determinants", ) - def __init__(self, spline, solution_field=None, orders=None): + def __init__(self, spline, orders=None): self._spline = spline - self._solution_field = solution_field - if solution_field is not None: - self._mapper = self._solution_field.mapper(reference=self._spline) - self._para_dim = spline.para_dim if self._para_dim == 3: raise NotImplementedError("Not yet tested for 3D") - if solution_field is None: - self._ukv = spline.unique_knots - else: - self._ukv = self._solution_field.unique_knots - n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] - self._n_elems = _np.prod(n_elems_per_dim) + self._ukv = self._spline.unique_knots + self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] # Gauss-Legendre quadrature points and weights - spline_for_quad = spline if solution_field is None else solution_field - if orders is None: quad_positions = [] quad_weights = [] - for dim_quadrature_order in _default_quadrature_orders( - spline_for_quad - ): + for dim_quadrature_order in _default_quadrature_orders(spline): quad_position, quad_weight = _np.polynomial.legendre.leggauss( deg=dim_quadrature_order ) @@ -716,17 +297,10 @@ def __init__(self, spline, solution_field=None, orders=None): self._quad_positions = _cartesian_product(quad_positions) self._quad_weights = _np.prod(_cartesian_product(quad_weights), axis=1) else: - self._quad_positions, self._quad_weights = ( - _get_quadrature_information(spline_for_quad, orders) + self._quad_positions, self._quad_weights = _get_quadrature_information( + spline, orders ) - # Precompute grid IDs - self._grid_ids = _cartesian_product( - [_np.arange(n_elems) for n_elems in n_elems_per_dim], - reverse=True, - ) - - self._all_supports = None self._all_element_quad_points = None self._all_jacobians = None self._all_jacobian_inverses = None @@ -741,13 +315,7 @@ def check_element_id_validity(self, element_id): ID of element in spline's element. ID-array is 1D """ assert element_id >= 0 - assert element_id < self._n_elems - - @property - def all_supports(self): - """Supports of all quadrature points. - List of entries of support""" - return self._all_supports + assert element_id < _np.prod(self._n_elems_per_dim) @property def all_quad_points(self): @@ -793,7 +361,10 @@ def get_element_grid_id(self, element_id): if self._para_dim == 3: raise NotImplementedError("Element grid ID not yet implemented for 3D") - return self._grid_ids[element_id, :] + n_elems_x = self._n_elems_per_dim[0] + grid_id = [element_id % n_elems_x, element_id // n_elems_x] + + return grid_id def get_element_quad_points(self, element_id): """For given element computes quad points @@ -824,36 +395,7 @@ def get_element_quad_points(self, element_id): element_lengths = _np.diff(element_corner_points, axis=1).ravel() element_midpoints = _np.mean(element_corner_points, axis=1) - # Bring center to origin and scale - element_quad_points = (self._quad_positions - 0.5) * element_lengths - # Apply offset - element_quad_points += element_midpoints - - return element_quad_points - - def get_element_support(self, element_id): - """Get support for quadrature points in element - - Parameters - ------------ - element_id: int - ID of element - - Returns - --------- - support: np.ndarray - Support for element. All quadrature points have same support - """ - element_quad_points = self.get_element_quad_points(element_id) - - # All quad points in element have same support, therefore take arbitrary - # one - relevant_quad_point = element_quad_points[0, :] - - if self._solution_field is None: - return self._spline.support(relevant_quad_point) - else: - return self._solution_field.support(relevant_quad_point) + return self._quad_positions / 2 * element_lengths + element_midpoints def jacobian(self, element_id): """Return Jacobian of single element at quadrature points @@ -950,28 +492,6 @@ def compute_all_element_quad_points(self, recompute=False): (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 ) - def compute_all_supports(self, recompute=False): - """Compute the support for all quadrature points - - Parameters - -------------- - recompute: bool - Recompute quadrature points - """ - if self._all_supports is not None and not recompute: - return - - self.compute_all_element_quad_points(recompute=recompute) - relevant_spline = ( - self._spline - if self._solution_field is None - else self._solution_field - ) - self._all_supports = [ - relevant_spline.support(quad_points[:1, :]).ravel() - for quad_points in self._all_element_quad_points - ] - def compute_all_element_jacobians(self, recompute=False): """Compute Jacobians of each element at each quadrature point @@ -1059,9 +579,7 @@ def __init__(self, geometry, solution_field=None, orders=None): self._helpee = geometry if solution_field is None: self._solution_field = geometry.copy() - self._solution_field.control_points = _np.ones( - (geometry.cps.shape[0], 1) - ) + self._solution_field.control_points = _np.ones((geometry.cps.shape[0], 1)) else: self._solution_field = solution_field self._ndofs = int( @@ -1077,14 +595,15 @@ def __init__(self, geometry, solution_field=None, orders=None): ) self._mapper = self._solution_field.mapper(reference=self._helpee) - self._trafo = Transformation(spline, orders) + self._trafo = Transformation(geometry, orders) self.precompute_transformation() + self._rhs = None + self._system_matrix = None + def reset(self, orders=None): """ """ - self._trafo = Transformation( - self._helpee, self._solution_field, orders - ) + self._trafo = Transformation(self._helpee, self._solution_field, orders) self.precompute_transformation() self._supports = None @@ -1166,16 +685,14 @@ def assemble_matrix(self, function, matrixout=None): # If other matrix is used, check if it compatible if matrixout is not None: if _has_scipy: - assert isinstance( - matrixout, _dok_matrix - ), "Matrixout must be scipy sparse dok matrix" + assert isinstance(matrixout, _dok_matrix), ( + "Matrixout must be scipy sparse dok matrix" + ) else: assert isinstance(matrixout, _np.ndarray) assert matrixout.shape == (self._ndofs, self._ndofs) - system_matrix = ( - self._global_system_matrix if matrixout is None else matrixout - ) + system_matrix = self._global_system_matrix if matrixout is None else matrixout quad_weights = self._trafo.quadrature_weights @@ -1276,17 +793,15 @@ def assemble_matrix_and_vector( # If other matrix is used, check if it compatible if matrixout is not None: if _has_scipy: - assert isinstance( - matrixout, _dok_matrix - ), "Matrixout must be scipy sparse dok matrix" + assert isinstance(matrixout, _dok_matrix), ( + "Matrixout must be scipy sparse dok matrix" + ) else: assert isinstance(matrixout, _np.ndarray) assert matrixout.shape == (self._ndofs, self._ndofs) # Set matrix accordingly - system_matrix = ( - self._global_system_matrix if matrixout is None else matrixout - ) + system_matrix = self._global_system_matrix if matrixout is None else matrixout # Initialize rhs vector if self._global_rhs is None: @@ -1350,9 +865,7 @@ def get_boundary_dofs(self): Indices of relevant boundary dofs """ relevant_spline = ( - self._helpee - if self._solution_field is None - else self._solution_field + self._helpee if self._solution_field is None else self._solution_field ) multi_index = relevant_spline.multi_index @@ -1408,9 +921,7 @@ def apply_dirichlet_boundary_conditions(self, function): # Assemble mass matrix global_size = (self._ndofs, self._ndofs) - mass_matrix = ( - _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) - ) + mass_matrix = _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) def mass_lhs(mapper, quad_points, quad_weights, jacobian_det): bf_values = mapper._field_reference.basis(quad_points) @@ -1431,9 +942,7 @@ def mass_lhs(mapper, quad_points, quad_weights, jacobian_det): def rhs_function(mapper, quad_points, quad_weights, jacobian_det): bf = mapper._field_reference.basis(quad_points) - quad_points_forward = mapper._geometry_reference.evaluate( - quad_points - ) + quad_points_forward = mapper._geometry_reference.evaluate(quad_points) function_values = function(quad_points_forward) element_vector = _np.einsum( "qj,q,q,q->j", diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index c329537a7..8778b7651 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -1,4 +1,5 @@ import numpy as np +import pytest import splinepy From 8d1413f37f47ebb3d0d3967dbe86eea5cd363daa Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 6 Aug 2024 13:30:29 +0200 Subject: [PATCH 155/171] Resolved all rebase issues --- splinepy/helpme/integrate.py | 150 ++++++++++++++++++++------------- tests/helpme/test_integrate.py | 134 ++++++++++++----------------- 2 files changed, 143 insertions(+), 141 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index cc6048720..08f4b9b45 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -21,7 +21,7 @@ def _get_integral_measure(spline): .. math:: \\mathcal{J}_S = det(\\mathbf(J)) - If the physical dimension is bigger then the paramtric dimension it will + If the physical dimension is bigger then the parametric dimension it will return .. math:: @@ -182,7 +182,7 @@ def parametric_function( """ from splinepy.spline import Spline as _Spline - # Check i_nput type + # Check input type if not isinstance(spline, _Spline): raise NotImplementedError("integration only works for splines") @@ -209,8 +209,8 @@ def parametric_function( ) else: - result = _np.sum( - function(positions) * meas(spline, positions) * weights, axis=1 + result = _np.einsum( + "id,i,i->d", function(positions), meas(spline, positions), weights ) return result @@ -261,31 +261,45 @@ def parametric_function(self, *args, **kwargs): class Transformation: __slots__ = ( "_spline", + "_solution_field", + "_mapper", "_para_dim", "_ukv", - "_n_elems_per_dim", + "_n_elems", "_quad_positions", "_quad_weights", + "_grid_ids", + "_all_supports", "_all_element_quad_points", "_all_jacobians", "_all_jacobian_inverses", "_all_jacobian_determinants", ) - def __init__(self, spline, orders=None): + def __init__(self, spline, solution_field=None, orders=None): self._spline = spline + self._solution_field = solution_field + if solution_field is not None: + self._mapper = self._solution_field.mapper(reference=self._spline) + self._para_dim = spline.para_dim if self._para_dim == 3: raise NotImplementedError("Not yet tested for 3D") - self._ukv = self._spline.unique_knots - self._n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + if solution_field is None: + self._ukv = spline.unique_knots + else: + self._ukv = self._solution_field.unique_knots + n_elems_per_dim = [len(kv) - 1 for kv in self._ukv] + self._n_elems = _np.prod(n_elems_per_dim) # Gauss-Legendre quadrature points and weights + spline_for_quad = spline if solution_field is None else solution_field + if orders is None: quad_positions = [] quad_weights = [] - for dim_quadrature_order in _default_quadrature_orders(spline): + for dim_quadrature_order in _default_quadrature_orders(spline_for_quad): quad_position, quad_weight = _np.polynomial.legendre.leggauss( deg=dim_quadrature_order ) @@ -298,9 +312,16 @@ def __init__(self, spline, orders=None): self._quad_weights = _np.prod(_cartesian_product(quad_weights), axis=1) else: self._quad_positions, self._quad_weights = _get_quadrature_information( - spline, orders + spline_for_quad, orders ) + # Precompute grid IDs + self._grid_ids = _cartesian_product( + [_np.arange(n_elems) for n_elems in n_elems_per_dim], + reverse=True, + ) + + self._all_supports = None self._all_element_quad_points = None self._all_jacobians = None self._all_jacobian_inverses = None @@ -315,7 +336,13 @@ def check_element_id_validity(self, element_id): ID of element in spline's element. ID-array is 1D """ assert element_id >= 0 - assert element_id < _np.prod(self._n_elems_per_dim) + assert element_id < self._n_elems + + @property + def all_supports(self): + """Supports of all quadrature points. + List of entries of support""" + return self._all_supports @property def all_quad_points(self): @@ -361,10 +388,7 @@ def get_element_grid_id(self, element_id): if self._para_dim == 3: raise NotImplementedError("Element grid ID not yet implemented for 3D") - n_elems_x = self._n_elems_per_dim[0] - grid_id = [element_id % n_elems_x, element_id // n_elems_x] - - return grid_id + return self._grid_ids[element_id, :] def get_element_quad_points(self, element_id): """For given element computes quad points @@ -395,7 +419,36 @@ def get_element_quad_points(self, element_id): element_lengths = _np.diff(element_corner_points, axis=1).ravel() element_midpoints = _np.mean(element_corner_points, axis=1) - return self._quad_positions / 2 * element_lengths + element_midpoints + # Bring center to origin and scale + element_quad_points = (self._quad_positions - 0.5) * element_lengths + # Apply offset + element_quad_points += element_midpoints + + return element_quad_points + + def get_element_support(self, element_id): + """Get support for quadrature points in element + + Parameters + ------------ + element_id: int + ID of element + + Returns + --------- + support: np.ndarray + Support for element. All quadrature points have same support + """ + element_quad_points = self.get_element_quad_points(element_id) + + # All quad points in element have same support, therefore take arbitrary + # one + relevant_quad_point = element_quad_points[0, :] + + if self._solution_field is None: + return self._spline.support(relevant_quad_point) + else: + return self._solution_field.support(relevant_quad_point) def jacobian(self, element_id): """Return Jacobian of single element at quadrature points @@ -492,6 +545,26 @@ def compute_all_element_quad_points(self, recompute=False): (offsets).reshape(n_elements, 1, -1), n_quad_points, 1 ) + def compute_all_supports(self, recompute=False): + """Compute the support for all quadrature points + + Parameters + -------------- + recompute: bool + Recompute quadrature points + """ + if self._all_supports is not None and not recompute: + return + + self.compute_all_element_quad_points(recompute=recompute) + relevant_spline = ( + self._spline if self._solution_field is None else self._solution_field + ) + self._all_supports = [ + relevant_spline.support(quad_points[:1, :]).ravel() + for quad_points in self._all_element_quad_points + ] + def compute_all_element_jacobians(self, recompute=False): """Compute Jacobians of each element at each quadrature point @@ -595,11 +668,7 @@ def __init__(self, geometry, solution_field=None, orders=None): ) self._mapper = self._solution_field.mapper(reference=self._helpee) - self._trafo = Transformation(geometry, orders) - self.precompute_transformation() - - self._rhs = None - self._system_matrix = None + self.reset(orders) def reset(self, orders=None): """ """ @@ -616,45 +685,6 @@ def precompute_transformation(self): self._trafo.compute_all_supports() self._trafo.compute_all_element_jacobian_determinants() - @property - def positions(self): - """ - Normalized Quadrature positions. Can use this value for - """ - return self._positions - - @property - def global_positions(self): - """ - Quadrature points in global position - """ - if self._global_positions is not None: - return self._global_positions - - # TODO: clamped knot vector check once it's merged - lower_bounds_per_dim = [] - span_scales_per_dim = [] - for ukv in self._helpee.unique_knots: - lower_bounds_per_dim.append(ukv[:-1]) - span_scales_per_dim.append(_np.diff(ukv)) - lower_bounds = _cartesian_product(lower_bounds_per_dim, reverse=True) - span_scales = _cartesian_product(span_scales_per_dim, reverse=True) - - # add lower bound as offsets using np.broadcast rules - n_quads, dim = self.positions.shape - n_elem = len(lower_bounds) - - # create normalized quad points for each element - self._global_positions = _np.tile(self.positions, (n_elem, 1)).reshape( - n_elem, n_quads, dim - ) - # scale them - self._global_positions *= span_scales.reshape(n_elem, 1, dim) - # apply offset - self._global_positions += lower_bounds.reshape(n_elem, 1, dim) - - return self._global_positions - @property def supports(self): """ """ diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 8778b7651..e9d2cfe80 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -4,6 +4,58 @@ import splinepy +def test_transformation_class(np_rng): + """Test element transformation of single patch""" + # Create quadratic spline + spline = splinepy.BSpline( + degrees=[2, 2], + knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], + control_points=splinepy.utils.data.cartesian_product( + [np.linspace(0, 1, 3), np.linspace(0, 1, 3)] + ), + ) + + # Randomly insert knots along both parametric dimensions + spline.insert_knots(0, np_rng.random(2)) + spline.insert_knots(1, np_rng.random(2)) + + # Create transformation class for spline + trafo = splinepy.helpme.integrate.Transformation(spline) + + # Check whether element quadrature points lie inside element + ukvs = spline.unique_knots + trafo.compute_all_element_quad_points() + # Check quadrature points of each element + for element_id, quad_points in enumerate(trafo.all_quad_points): + grid_id = trafo.get_element_grid_id(element_id) + # Extract the corners of the current element + element_corners = [ + ukv[grid_dim_id : grid_dim_id + 2] + for ukv, grid_dim_id in zip(ukvs, grid_id) + ] + # Check if quadrature points lie within element + for dim, corners in enumerate(element_corners): + assert np.all( + (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) + ), f"Quadrature points do not lie within element for dimension {dim}" + + # For given spline, all Jacobians should be identity matrix + eye = np.eye(spline.para_dim) + trafo.compute_all_element_jacobian_inverses() + for element_jacobians in trafo.all_jacobians: + for jacobian_at_quad_point in element_jacobians: + assert np.allclose(eye, jacobian_at_quad_point), ( + "All Jacobians should be identity matrix" + ) + + # For created spline, all determinants should equal one + trafo.compute_all_element_jacobian_determinants() + assert np.allclose( + trafo.all_jacobian_determinants, + np.ones_like(trafo.all_jacobian_determinants), + ), "All Jacobians' determinants should be equal to one" + + def test_volume_integration_1D(np_rng): """ Test volume integration for splines using numerical integration of the @@ -181,90 +233,10 @@ def volume_function(x): [bezier.integrate.volume(), col1_factor * bezier.integrate.volume()], bezier.integrate.parametric_function(volume_function), ) + # try bsplines bspline = bezier.bspline assert np.allclose( [bspline.integrate.volume(), col1_factor * bspline.integrate.volume()], bspline.integrate.parametric_function(volume_function), ) - - -def test_transformation_class(): - """Test element transformation of single patch""" - # Create quadratic spline - spline = splinepy.BSpline( - degrees=[2, 2], - knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], - control_points=splinepy.utils.data.cartesian_product( - [np.linspace(0, 1, 3), np.linspace(0, 1, 3)] - ), - ) - - # Randomly insert knots along both parametric dimensions - spline.insert_knots(0, np.random.random(2)) - spline.insert_knots(1, np.random.random(2)) - - # Create transformation class for spline - trafo = splinepy.helpme.integrate.Transformation(spline) - - # Check whether element quadrature points lie inside element - ukvs = spline.unique_knots - trafo.compute_all_element_quad_points() - for element_id, quad_points in enumerate(trafo.all_quad_points): - grid_id = trafo.get_element_grid_id(element_id) - element_corners = [ - ukv[grid_dim_id : grid_dim_id + 2] - for ukv, grid_dim_id in zip(ukvs, grid_id) - ] - # Check if quadrature points lie within element corners - for dim, corners in enumerate(element_corners): - assert np.all( - (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) - ) - - # For given spline, all Jacobians should be identity matrix - eye = np.eye(spline.para_dim) - trafo.compute_all_element_jacobian_inverses() - for element_jacobians in trafo.all_jacobians: - for jacobian_at_quad_point in element_jacobians: - assert np.allclose(eye, jacobian_at_quad_point) - - # For created spline, all determinants should equal one - trafo.compute_all_element_jacobian_determinants() - assert np.allclose( - trafo.all_jacobian_determinants, - np.ones_like(trafo.all_jacobian_determinants), - ) - - -def test_physical_function_integration(np_rng): - # Analytical integral of y*(5-y) over [0,1]x[0,5] rectangle - integral_analytical = 125 / 6 - - def parabolic_function(points): - if isinstance(points, list): - y = points[0][1] - elif isinstance(points, np.ndarray): - y = points[:, 1] - else: - raise TypeError("Unsupported type for points") - return (y * (5 - y)).reshape(-1, 1) - - # Create rectangle domain - xlin, ylin = np.linspace(0, 1, 3), np.linspace(0, 5, 3) - control_points = np.vstack([array.ravel() for array in np.meshgrid(xlin, ylin)]).T - rectangle = splinepy.BSpline( - degrees=[2, 2], - knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], - control_points=control_points, - ) - - # Insert random knots along both parametric dimensions - rectangle.insert_knots(0, np_rng.random(2)) - rectangle.insert_knots(1, np_rng.random(2)) - - integral = splinepy.helpme.integrate.physical_function( - rectangle, parabolic_function - ) - - assert np.allclose(integral, integral_analytical) From 4350d226fabbd3ce94d7934d63be896df0ab7979 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 6 Aug 2024 15:46:56 +0200 Subject: [PATCH 156/171] Add comments --- ...lerkin_laplace_problem_field_integrator.py | 39 +++++++++++++++++++ splinepy/helpme/integrate.py | 16 +++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/examples/iga/galerkin_laplace_problem_field_integrator.py b/examples/iga/galerkin_laplace_problem_field_integrator.py index d3e505eef..acad6cb68 100644 --- a/examples/iga/galerkin_laplace_problem_field_integrator.py +++ b/examples/iga/galerkin_laplace_problem_field_integrator.py @@ -51,6 +51,25 @@ def prepare_geometry_and_solution_field(): def poisson_lhs(mapper, quad_points, quad_weights, jacobian_det): + """ + Assemble the system matrix of the Poisson equation. + + Parameters + ----------- + mapper: splinepy.helpme.mapper.Mapper + Mapper of solution field + quad_points: np.ndarray + Quadrature points + quad_weights: np.ndarray + Quadrature weights + jacobian_det: np.ndarray + Determinant of Jacobian, evaluated at quadrature points + + Returns + ----------- + element_matrix: np.ndarray + Element matrix + """ bf_gradient, _ = mapper.basis_gradient_and_support(quad_points) element_matrix = np.einsum( "qid,qjd,q,q->ij", @@ -64,6 +83,25 @@ def poisson_lhs(mapper, quad_points, quad_weights, jacobian_det): def poisson_rhs(mapper, quad_points, quad_weights, jacobian_det): + """ + Assemble the rhs of the Poisson equation with f=1 + + Parameters + ----------- + mapper: splinepy.helpme.mapper.Mapper + Mapper of solution field + quad_points: np.ndarray + Quadrature points + quad_weights: np.ndarray + Quadrature weights + jacobian_det: np.ndarray + Determinant of Jacobian, evaluated at quadrature points + + Returns + ----------- + element_vector: np.ndarray + Element vector + """ bf = mapper._field_reference.basis(quad_points) element_vector = np.einsum( "qj,q,q->j", bf, quad_weights, jacobian_det, optimize=True @@ -72,6 +110,7 @@ def poisson_rhs(mapper, quad_points, quad_weights, jacobian_det): def show_solution(geometry, solution_field): + """Visualize the solution""" geometry.spline_data["field"] = solution_field geometry.show_options["data"] = "field" geometry.show_options["cmap"] = "jet" diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 08f4b9b45..985f7b89f 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -71,7 +71,7 @@ def _get_quadrature_information(spline, orders=None): """ Select appropriate integration order (gauss-legendre) - Determinante of a polynomial spline with para_dim==dim has degree + Determinant of a polynomial spline with para_dim==dim has degree .. math:: p_i^{det} = n_{dim} \\cdot p_i - 1 @@ -210,7 +210,10 @@ def parametric_function( else: result = _np.einsum( - "id,i,i->d", function(positions), meas(spline, positions), weights + "i...,i,i->...", + function(positions), + meas(spline, positions), + weights, ) return result @@ -929,15 +932,16 @@ def apply_dirichlet_boundary_conditions(self, function): """ Applies Dirichlet boundary conditions via :math:`L^2`-projection. - For a given function g, it solves the following equation + For a given function g, :math:`L^2`-projection is obtained by solving + the following equation: - .. math:: \\sum\\limits_{j=1}^n \alpha_j (N_i, N_j) = (g, N_i) \\quad + .. math:: \\sum\\limits_{j=1}^n \\alpha_j (N_i, N_j) = (g, N_i) \\quad \forall i = 1, \\dots, n. - Then, the :math:`Pg`, the :math:`L^2`-projection of the function, is + Then, :math:`Pg`, the :math:`L^2`-projection of the function, is given by - .. math:: Pg(x) = \\sum\\limits_{j=1}^n \alpha_j \\phi_j(x) + .. math:: Pg(x) = \\sum\\limits_{j=1}^n \\alpha_j \\phi_j(x). For the Dirichlet boundary conditions, only the DoFs corresponding to the boundaries are taken from the :math:`L^2`-projection. From 52ff34ef2b2b1960705b92ab3397f085c05d83c2 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 13 Aug 2024 10:28:27 +0200 Subject: [PATCH 157/171] Restructure FieldIntegrator's Dirichlet function --- splinepy/helpme/integrate.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 985f7b89f..497b1e833 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -953,11 +953,8 @@ def apply_dirichlet_boundary_conditions(self, function): """ self.check_if_assembled() - # Assemble mass matrix - global_size = (self._ndofs, self._ndofs) - mass_matrix = _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) - - def mass_lhs(mapper, quad_points, quad_weights, jacobian_det): + def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): + # Assemble system matrix bf_values = mapper._field_reference.basis(quad_points) element_matrix = _np.einsum( "qi,qj,q,q->ij", @@ -967,28 +964,29 @@ def mass_lhs(mapper, quad_points, quad_weights, jacobian_det): jacobian_det, optimize=True, ) - return element_matrix.ravel() - - self.assemble_matrix(mass_lhs, matrixout=mass_matrix) - - # Assemble rhs: f(x) * N - rhs_vector = _np.zeros(self._ndofs) - def rhs_function(mapper, quad_points, quad_weights, jacobian_det): - bf = mapper._field_reference.basis(quad_points) + # Assemble rhs quad_points_forward = mapper._geometry_reference.evaluate(quad_points) function_values = function(quad_points_forward) element_vector = _np.einsum( "qj,q,q,q->j", - bf, + bf_values, function_values, quad_weights, jacobian_det, optimize=True, ) - return element_vector - self.assemble_vector(rhs_function, vectorout=rhs_vector) + return element_matrix.ravel(), element_vector + + # Initialiye mass matrix and rhs + global_size = (self._ndofs, self._ndofs) + mass_matrix = _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) + rhs_vector = _np.zeros(self._ndofs) + # Assemble lhs and rhs + self.assemble_matrix_and_vector( + dirichlet_lhs_and_rhs, matrixout=mass_matrix, vectorout=rhs_vector + ) # Solve system to get all dofs dof_vector = _np.empty(self._ndofs) From 095e35258ddc3d9da089bbc1d82d97ab376f306f Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 13 Aug 2024 10:42:11 +0200 Subject: [PATCH 158/171] Add return values option to FieldIntegrator's Dirichlet function --- splinepy/helpme/integrate.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 497b1e833..a74cd5b7b 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -928,7 +928,16 @@ def assign_homogeneous_dirichlet_boundary_conditions(self): self._global_system_matrix[indices, indices] = 1 self._global_rhs[indices] = 0 - def apply_dirichlet_boundary_conditions(self, function): + def apply_dirichlet_boundary_conditions( + self, + function, + return_values=False, + all_boundaries=True, + north=False, + east=False, + south=False, + west=False, + ): """ Applies Dirichlet boundary conditions via :math:`L^2`-projection. @@ -950,6 +959,18 @@ def apply_dirichlet_boundary_conditions(self, function): ------------- function: callable Function to apply. Input are points, output is scalar + return_values: bool + If True, computed values and the corresponding DoF indices will be returned + and not applied to global system matrix or the global rhs. If False, values + will be applied to system matrix and rhs and nothing will be returned + all_boundaries: bool + If True, get indices of all boundary dofs and ignores values for + north, south, east and west + north: bool + If True, sets homogeneous Dirichlet condition on north boundary + east: bool + south: bool + west: bool """ self.check_if_assembled() @@ -998,10 +1019,13 @@ def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): # Get relevant dofs indices = self.get_boundary_dofs() - # Apply Dirichlet to relevant boundary dofs - self._global_system_matrix[indices, :] = 0 - self._global_system_matrix[indices, indices] = 1 - self._global_rhs[indices] = dof_vector[indices] + if return_values: + return dof_vector[indices], indices + else: + # Apply Dirichlet to relevant boundary dofs + self._global_system_matrix[indices, :] = 0 + self._global_system_matrix[indices, indices] = 1 + self._global_rhs[indices] = dof_vector[indices] def solve_linear_system(self): """ From 9391afda57209a3d068c041538d26bc5d7c494f2 Mon Sep 17 00:00:00 2001 From: markriegler Date: Thu, 15 Aug 2024 10:08:34 +0200 Subject: [PATCH 159/171] Add L2-projection to FieldIntegrator --- splinepy/helpme/integrate.py | 105 +++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 43 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index a74cd5b7b..6c02d3122 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -655,7 +655,7 @@ def __init__(self, geometry, solution_field=None, orders=None): self._helpee = geometry if solution_field is None: self._solution_field = geometry.copy() - self._solution_field.control_points = _np.ones((geometry.cps.shape[0], 1)) + self._solution_field.control_points = _np.zeros((geometry.cps.shape[0], 1)) else: self._solution_field = solution_field self._ndofs = int( @@ -881,6 +881,64 @@ def assemble_matrix_and_vector( # Add element vector to global rhs vector rhs_vector[element_support] += element_vector + def L2_projection(self, function): + """Perform an L2-projection of a function + + Parameters + ---------------- + function: callable + Function to L2-project + + Returns + ------------- + dof_values: np.ndarray + L2-projected values for Dofs + """ + + def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): + # Assemble system matrix + bf_values = mapper._field_reference.basis(quad_points) + element_matrix = _np.einsum( + "qi,qj,q,q->ij", + bf_values, + bf_values, + quad_weights, + jacobian_det, + optimize=True, + ) + + # Assemble rhs + quad_points_forward = mapper._geometry_reference.evaluate(quad_points) + function_values = function(quad_points_forward) + element_vector = _np.einsum( + "qj,q,q,q->j", + bf_values, + function_values, + quad_weights, + jacobian_det, + optimize=True, + ) + + return element_matrix.ravel(), element_vector + + # Initialiye mass matrix and rhs + global_size = (self._ndofs, self._ndofs) + mass_matrix = _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) + rhs_vector = _np.zeros(self._ndofs) + # Assemble lhs and rhs + self.assemble_matrix_and_vector( + dirichlet_lhs_and_rhs, matrixout=mass_matrix, vectorout=rhs_vector + ) + + # Solve system to get all dofs + dof_values = _np.empty(self._ndofs) + if _has_scipy: + dof_values = _spsolve(mass_matrix.tocsr(), rhs_vector) + else: + dof_values = _np.linalg.solve(mass_matrix, rhs_vector) + + return dof_values + def check_if_assembled(self): """ Check if system matrix and rhs are already assembled @@ -972,49 +1030,10 @@ def apply_dirichlet_boundary_conditions( south: bool west: bool """ - self.check_if_assembled() - - def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): - # Assemble system matrix - bf_values = mapper._field_reference.basis(quad_points) - element_matrix = _np.einsum( - "qi,qj,q,q->ij", - bf_values, - bf_values, - quad_weights, - jacobian_det, - optimize=True, - ) + if not return_values: + self.check_if_assembled() - # Assemble rhs - quad_points_forward = mapper._geometry_reference.evaluate(quad_points) - function_values = function(quad_points_forward) - element_vector = _np.einsum( - "qj,q,q,q->j", - bf_values, - function_values, - quad_weights, - jacobian_det, - optimize=True, - ) - - return element_matrix.ravel(), element_vector - - # Initialiye mass matrix and rhs - global_size = (self._ndofs, self._ndofs) - mass_matrix = _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) - rhs_vector = _np.zeros(self._ndofs) - # Assemble lhs and rhs - self.assemble_matrix_and_vector( - dirichlet_lhs_and_rhs, matrixout=mass_matrix, vectorout=rhs_vector - ) - - # Solve system to get all dofs - dof_vector = _np.empty(self._ndofs) - if _has_scipy: - dof_vector = _spsolve(mass_matrix.tocsr(), rhs_vector) - else: - dof_vector = _np.linalg.solve(mass_matrix, rhs_vector) + dof_vector = self.L2_projection(function) # Get relevant dofs indices = self.get_boundary_dofs() From d5a76c1a74d7fd18762b34acab3060754bcb1005 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 16 Aug 2024 11:09:08 +0200 Subject: [PATCH 160/171] Add error calcuation to FieldIntegrator --- splinepy/helpme/integrate.py | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 6c02d3122..27b3c6e5b 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -277,6 +277,7 @@ class Transformation: "_all_jacobians", "_all_jacobian_inverses", "_all_jacobian_determinants", + "_all_element_measures", ) def __init__(self, spline, solution_field=None, orders=None): @@ -329,6 +330,7 @@ def __init__(self, spline, solution_field=None, orders=None): self._all_jacobians = None self._all_jacobian_inverses = None self._all_jacobian_determinants = None + self._all_element_measures = None def check_element_id_validity(self, element_id): """Check if given element ID is valid @@ -637,6 +639,18 @@ def compute_all_element_jacobian_determinants(self, recompute=False): ] ) + def compute_all_element_measures(self, recompute=False): + """Computes the measures of all elements in parametric space. Assumes + tensor-product-like structure. + """ + if self._all_element_measures is not None and not recompute: + return + + element_lengths = [_np.diff(ukv) for ukv in self._ukv] + self._all_element_measures = _np.prod( + _cartesian_product(element_lengths), axis=1 + ) + class FieldIntegrator(_SplinepyBase): __slots__ = ( @@ -1061,3 +1075,61 @@ def solve_linear_system(self): self._global_system_matrix, self._global_rhs ) self._solution_field.control_points = solution_vector.reshape(-1, 1) + + def compute_error(self, function, norm="l2"): + """ + Compute error to some given function(s) w.r.t a norm + + Parameters + ------------- + function: callable + Analytical function(s) to compare to + norm: str + Used norm for error calculation + + Returns + ------------------ + error_integration: np.ndarray + Value(s) of error in given norm + """ + self._trafo.compute_all_element_jacobian_determinants() + + # Evaluate function and solution values + all_quad_points = self._trafo._all_element_quad_points.reshape( + -1, self._helpee.para_dim + ) + all_physical_points = self._helpee.evaluate(all_quad_points) + all_function_values = function(all_physical_points) + solution_values = self._solution_field.evaluate(all_quad_points) + error_values = all_function_values - solution_values + # Take the norm into consideration + if norm == "l1": + error_norm_values = _np.abs(error_values) + elif norm == "l2": + error_norm_values = _np.power(error_values, 2) + elif norm == "linf": + return _np.max(_np.abs(error_values)) + else: + raise NotImplementedError(f"{norm}-norm not implemented") + + # Integrate error + quad_weights = self._trafo._quad_weights + n_weights = quad_weights.shape[0] + quad_weights = _np.tile( + quad_weights, len(solution_values) // len(quad_weights) + ) + self._trafo.compute_all_element_measures() + + error_integration = _np.einsum( + "i...,i,i->...", + error_norm_values, + self._trafo._all_jacobian_determinants.ravel() + * _np.repeat(self._trafo._all_element_measures, n_weights), + quad_weights, + optimize=True, + ) + + if norm == "l1": + return error_integration + elif norm == "l2": + return _np.sqrt(error_integration) From e9fd081f0f775357b614ab90eb5cb913d96a14e0 Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 16 Aug 2024 17:14:24 +0200 Subject: [PATCH 161/171] Getting it to the state I wanted it to be without all the rebase issues --- ...lerkin_laplace_problem_field_integrator.py | 22 +- splinepy/helpme/integrate.py | 198 +++++++++++++----- tests/helpme/test_integrate.py | 114 +++++----- 3 files changed, 223 insertions(+), 111 deletions(-) diff --git a/examples/iga/galerkin_laplace_problem_field_integrator.py b/examples/iga/galerkin_laplace_problem_field_integrator.py index acad6cb68..e6dff58b0 100644 --- a/examples/iga/galerkin_laplace_problem_field_integrator.py +++ b/examples/iga/galerkin_laplace_problem_field_integrator.py @@ -5,15 +5,15 @@ -\Delta u = f with source term f=1. -First, homogeneous Dirichlet boundary conditions are applied. Then, inhomogeneous ones -are applied. +First, homogeneous Dirichlet boundary conditions are applied. Then, homogeneous +and inhomogeneous Dirichlet boundary conditions are applied on 3 of 4 boundaries. """ import numpy as np import splinepy as sp -# Test Case +# Number of refinements for the solution field n_refine = 15 @@ -130,7 +130,7 @@ def show_solution(geometry, solution_field): fi.assemble_vector(poisson_rhs) # Homogeneous Dirichlet boundary conditions - fi.assign_homogeneous_dirichlet_boundary_conditions() + fi.apply_homogeneous_dirichlet_boundary_conditions() fi.solve_linear_system() # Plot geometry and field show_solution(geometry, solution_field) @@ -138,10 +138,18 @@ def show_solution(geometry, solution_field): # Inhomogeneous Dirichlet boundary conditions def dirichlet_function(points): """ - On the boundary apply: g(x,y) = x/10 + On the boundary apply: g(x,y) = x/4 """ - return points[:, 0] / 10 + return points[:, 0] / 4 - fi.apply_dirichlet_boundary_conditions(dirichlet_function) + # Assemble again to override previous boundary conditions + fi.assemble_matrix(poisson_lhs) + fi.assemble_vector(poisson_rhs) + + # Apply boundary conditions on 3 boundaries + fi.apply_homogeneous_dirichlet_boundary_conditions(west=True) + fi.apply_dirichlet_boundary_conditions( + dirichlet_function, south=True, north=True + ) fi.solve_linear_system() show_solution(geometry, solution_field) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 27b3c6e5b..670acbf5c 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -2,7 +2,6 @@ import numpy as _np -from splinepy._base import SplinepyBase as _SplinepyBase from splinepy.utils.data import cartesian_product as _cartesian_product from splinepy.utils.data import has_scipy as _has_scipy @@ -21,7 +20,7 @@ def _get_integral_measure(spline): .. math:: \\mathcal{J}_S = det(\\mathbf(J)) - If the physical dimension is bigger then the parametric dimension it will + If the physical dimension is bigger than the parametric dimension it will return .. math:: @@ -30,12 +29,13 @@ def _get_integral_measure(spline): Parameters ---------- spline : Spline / Multipatch - For parametric and physical dimension + The spline object for which the measure of one of its patches is determined Returns ------- measure : Callable - single patch only + A function which computes the jacobian's determinant for a single patch. + It takes a patch and query positions in the parametric domain as input """ # Check dimensionality if spline.dim == spline.para_dim: @@ -58,6 +58,7 @@ def measure(spline_patch, positions): def _default_quadrature_orders(spline): + # Expected degree for quadrature based on spline's degrees and parametric dimension expected_degree = spline.degrees * spline.para_dim + 1 if spline.is_rational: expected_degree += 2 @@ -69,7 +70,10 @@ def _default_quadrature_orders(spline): def _get_quadrature_information(spline, orders=None): """ - Select appropriate integration order (gauss-legendre) + Select appropriate integration order (gauss-legendre). + + Determines the integration points and weights for numerical integration + based on the given spline and the optionally given quadrature orders. Determinant of a polynomial spline with para_dim==dim has degree @@ -87,16 +91,17 @@ def _get_quadrature_information(spline, orders=None): Parameters ---------- spline : Spline - Spline for integration + The spline object for which the quadrature information is determined. orders : array-like (optional) - Orders along every parametric dimension + Orders along every parametric dimension. If not provided, default quadrature + orders will be used. Returns ------- positions : np.ndarray - quadrature position in unit-square + Quadrature points in unit-square weights : np.ndarray - quadrature weights + Quadrature weights """ # Determine integration points @@ -129,22 +134,24 @@ def _get_quadrature_information(spline, orders=None): def volume(spline, orders=None): r"""Compute volume of a given spline + Uses quadrature to compute volume. Can handle patches with multiple elements. + Parameters ---------- spline : Spline (self if called via integrator) - splinepy - spline type + The spline object for which the volume is computed. orders : array-like (optional) order for gauss quadrature Returns ------- volume : float - Integral of dim-dimensional object + The computed volume of the spline """ from splinepy.spline import Spline as _Spline - # Check i_nput type + # Check input type if not isinstance(spline, _Spline): raise NotImplementedError("integration only works for splines") @@ -172,9 +179,12 @@ def parametric_function( Parameters ---------- spline : Spline - (self if called via integrator) + The geometry over which the function is integrated function : Callable + The user-defined function to integrate. It takes points in the parametric + dimension as input and outputs a scalar or an array of scalars. orders : optional + Quadrature orders for numerical integration Returns ------- @@ -281,6 +291,19 @@ class Transformation: ) def __init__(self, spline, solution_field=None, orders=None): + """ + Setup transformation class with a given geometry. + + Parameters + ------------ + spline: spline + The geometry + solution_field: None or spline + Solution field as spline function. If not given, supports and quadrature + will be calculated for geometry + orders: None or list + Quadrature orders. If not given, default quadrature orders will be used + """ self._spline = spline self._solution_field = solution_field if solution_field is not None: @@ -378,17 +401,20 @@ def quadrature_weights(self): return self._quad_weights def get_element_grid_id(self, element_id): - """Compute element ID in grid + """Compute element ID in grid. + + The grid follows splinepy's ordering: first it goes into the x-direction, + after that into the y-direction. Parameters ---------- element_id: int - ID of element of spline + ID of spline's element in element grid Returns --------- element_grid_id: list - ID of element in grid + The grid ID of the element. """ if self._para_dim == 3: raise NotImplementedError("Element grid ID not yet implemented for 3D") @@ -396,12 +422,12 @@ def get_element_grid_id(self, element_id): return self._grid_ids[element_id, :] def get_element_quad_points(self, element_id): - """For given element computes quad points + """Compute the quadrature points for a given element. Parameters ----------- element_id: int - ID of element in spline's element + ID of spline's element in element grid Returns ----------- @@ -437,7 +463,7 @@ def get_element_support(self, element_id): Parameters ------------ element_id: int - ID of element + ID of spline's element in element grid Returns --------- @@ -461,7 +487,7 @@ def jacobian(self, element_id): Parameters ------------ element_id: list - ID of element in grid + ID of spline's element in element grid Returns --------- @@ -481,7 +507,7 @@ def jacobian_inverse(self, element_id): Parameters ------------ element_id: list - ID of element in grid + ID of spline's element in element grid Returns --------- @@ -525,8 +551,8 @@ def compute_all_element_quad_points(self, recompute=False): Parameters ---------- - recompute: bool - Recompute quadrature points + recompute: bool (optional) + If True, recompute the qudrature points. Default is no recomputation """ if self._all_element_quad_points is not None and not recompute: return @@ -555,8 +581,8 @@ def compute_all_supports(self, recompute=False): Parameters -------------- - recompute: bool - Recompute quadrature points + recompute: bool (optional) + If True, recomputes the supports. The default is no recomputation """ if self._all_supports is not None and not recompute: return @@ -575,8 +601,8 @@ def compute_all_element_jacobians(self, recompute=False): Parameters ---------- - recompute: bool - Recompute Jacobians + recompute: bool (optional) + If True, recomputes the Jacobians. Default option is no recomputation """ if self._all_jacobians is not None and not recompute: return @@ -594,8 +620,8 @@ def compute_all_element_jacobian_inverses(self, recompute=False): Parameters ---------- - recompute: bool - Recompute Jacobians' inverses + recompute: bool (optional) + If True, recompute Jacobians' inverses. Default is no recomputation """ if self._all_jacobian_inverses is not None and not recompute: return @@ -652,7 +678,7 @@ def compute_all_element_measures(self, recompute=False): ) -class FieldIntegrator(_SplinepyBase): +class FieldIntegrator: __slots__ = ( "_helpee", "_solution_field", @@ -665,7 +691,21 @@ class FieldIntegrator(_SplinepyBase): ) def __init__(self, geometry, solution_field=None, orders=None): - """ """ + """ + Sets up solution field, its mapper, precomputes the transformation and + calculate the number of DoFs. + + Parameters + ---------- + geometry: spline + The geometry + solution_field: None or spline + Solution field. If not given, quadrature and supports will be calculated + using the geometry + orders: None or list + Quadrature order in each dimension. If not given, default quadrature + will be used + """ self._helpee = geometry if solution_field is None: self._solution_field = geometry.copy() @@ -688,7 +728,14 @@ def __init__(self, geometry, solution_field=None, orders=None): self.reset(orders) def reset(self, orders=None): - """ """ + """Sets up the transformation and resets the lhs and rhs. + + Parameters + ------------ + orders: None or list + If given, these orders will be used for quadrature. Otherwise, default + quadrature orders will be used. + """ self._trafo = Transformation(self._helpee, self._solution_field, orders) self.precompute_transformation() @@ -704,7 +751,14 @@ def precompute_transformation(self): @property def supports(self): - """ """ + """ + Get the quadrature points' supports. + + Returns + ------- + supports: np.ndarray + The supports + """ if self._supports is None: self._supports = self._helpee.supports(self._trafo.all_quad_points) @@ -960,9 +1014,31 @@ def check_if_assembled(self): if self._global_system_matrix is None or self._global_rhs is None: raise ValueError("System is not yet fully assembled") - def get_boundary_dofs(self): + def get_boundary_dofs( + self, + all_boundaries=True, + north=False, + east=False, + south=False, + west=False, + ): """ - Get indices of boundary dofs. + Get indices of boundary dofs. Assumes that control points are arranged + in the following way: first one is the southwest corner, then the first + dimension goes from west to east and second dimension goes from south to + north. + + Parameters + ------------------ + all_boundaries: bool + If True, get indices of all boundary dofs and ignores values for + north, south, east and west. On the other, if any boundary is selected, + this value will be ignored + north: bool + If True, return indices for north boundary of geometry + east: bool + south: bool + west: bool Returns ------------- @@ -975,26 +1051,56 @@ def get_boundary_dofs(self): multi_index = relevant_spline.multi_index + multi_indices = [ + multi_index[0, :], + multi_index[-1, :], + multi_index[:, 0], + multi_index[:, -1], + ] + + # If at least one boundary is True, set all_boundaries to False + if north or east or south or west: + all_boundaries = False + + boundaries = [True] * 4 if all_boundaries else [west, east, south, north] + indices = _np.unique( _np.hstack( - ( - multi_index[0, :], - multi_index[-1, :], - multi_index[:, 0], - multi_index[:, -1], - ) + [ + index + for index, boundary in zip(multi_indices, boundaries) + if boundary + ] ) ) return indices - def assign_homogeneous_dirichlet_boundary_conditions(self): + def apply_homogeneous_dirichlet_boundary_conditions( + self, + all_boundaries=True, + north=False, + east=False, + south=False, + west=False, + ): """ Assembles homogeneous Dirichlet boundary conditions + + Parameters + ------------- + all_boundaries: bool + If True, get indices of all boundary dofs and ignores values for + north, south, east and west + north: bool + If True, sets homogeneous Dirichlet condition on north boundary + east: bool + south: bool + west: bool """ self.check_if_assembled() - indices = self.get_boundary_dofs() + indices = self.get_boundary_dofs(all_boundaries, north, east, south, west) self._global_system_matrix[indices, :] = 0 self._global_system_matrix[indices, indices] = 1 @@ -1050,7 +1156,7 @@ def apply_dirichlet_boundary_conditions( dof_vector = self.L2_projection(function) # Get relevant dofs - indices = self.get_boundary_dofs() + indices = self.get_boundary_dofs(all_boundaries, north, east, south, west) if return_values: return dof_vector[indices], indices @@ -1115,9 +1221,7 @@ def compute_error(self, function, norm="l2"): # Integrate error quad_weights = self._trafo._quad_weights n_weights = quad_weights.shape[0] - quad_weights = _np.tile( - quad_weights, len(solution_values) // len(quad_weights) - ) + quad_weights = _np.tile(quad_weights, len(solution_values) // len(quad_weights)) self._trafo.compute_all_element_measures() error_integration = _np.einsum( diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index e9d2cfe80..85fb26a42 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -4,58 +4,6 @@ import splinepy -def test_transformation_class(np_rng): - """Test element transformation of single patch""" - # Create quadratic spline - spline = splinepy.BSpline( - degrees=[2, 2], - knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], - control_points=splinepy.utils.data.cartesian_product( - [np.linspace(0, 1, 3), np.linspace(0, 1, 3)] - ), - ) - - # Randomly insert knots along both parametric dimensions - spline.insert_knots(0, np_rng.random(2)) - spline.insert_knots(1, np_rng.random(2)) - - # Create transformation class for spline - trafo = splinepy.helpme.integrate.Transformation(spline) - - # Check whether element quadrature points lie inside element - ukvs = spline.unique_knots - trafo.compute_all_element_quad_points() - # Check quadrature points of each element - for element_id, quad_points in enumerate(trafo.all_quad_points): - grid_id = trafo.get_element_grid_id(element_id) - # Extract the corners of the current element - element_corners = [ - ukv[grid_dim_id : grid_dim_id + 2] - for ukv, grid_dim_id in zip(ukvs, grid_id) - ] - # Check if quadrature points lie within element - for dim, corners in enumerate(element_corners): - assert np.all( - (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) - ), f"Quadrature points do not lie within element for dimension {dim}" - - # For given spline, all Jacobians should be identity matrix - eye = np.eye(spline.para_dim) - trafo.compute_all_element_jacobian_inverses() - for element_jacobians in trafo.all_jacobians: - for jacobian_at_quad_point in element_jacobians: - assert np.allclose(eye, jacobian_at_quad_point), ( - "All Jacobians should be identity matrix" - ) - - # For created spline, all determinants should equal one - trafo.compute_all_element_jacobian_determinants() - assert np.allclose( - trafo.all_jacobian_determinants, - np.ones_like(trafo.all_jacobian_determinants), - ), "All Jacobians' determinants should be equal to one" - - def test_volume_integration_1D(np_rng): """ Test volume integration for splines using numerical integration of the @@ -86,18 +34,18 @@ def test_volume_integration_embedded(np_rng): Test volume integration for splines using numerical integration of the Jacobi-Determinant """ - # Test 1D -> 2D + # Test 1D -> 2D volume integration for Bezier expected_result = 2.0**1.5 bezier = splinepy.Bezier(degrees=[1], control_points=[[0, 0], [2, 2]]) assert np.allclose(bezier.integrate.volume(), expected_result) - # test for other types same spline + # For same curve, test other spline types assert np.allclose(bezier.bspline.integrate.volume(), expected_result) assert np.allclose(bezier.rationalbezier.integrate.volume(), expected_result) assert np.allclose(bezier.nurbs.integrate.volume(), expected_result) - # Check if equal after refinement + # Check if volume is equal after degree elevation bezier.elevate_degrees([0, 0, 0]) assert np.allclose(bezier.integrate.volume(), expected_result) @@ -221,6 +169,7 @@ def test_assertions(np_rng): def test_function_integration(np_rng): col1_factor = 2 + # Define vector-valued constant function def volume_function(x): vf = np.ones((len(x), 2)) # scale it with a factor to get a different value @@ -228,15 +177,66 @@ def volume_function(x): return vf bezier = splinepy.Bezier(degrees=[1, 2], control_points=np_rng.random((6, 2))) - + # Test function integration for constant functions assert np.allclose( [bezier.integrate.volume(), col1_factor * bezier.integrate.volume()], bezier.integrate.parametric_function(volume_function), ) - # try bsplines bspline = bezier.bspline assert np.allclose( [bspline.integrate.volume(), col1_factor * bspline.integrate.volume()], bspline.integrate.parametric_function(volume_function), ) + + +def test_transformation_class(np_rng): + """Test element transformation of single patch""" + # Create quadratic spline + spline = splinepy.BSpline( + degrees=[2, 2], + knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], + control_points=splinepy.utils.data.cartesian_product( + [np.linspace(0, 1, 3), np.linspace(0, 1, 3)] + ), + ) + + # Randomly insert knots along both parametric dimensions + spline.insert_knots(0, np_rng.random(2)) + spline.insert_knots(1, np_rng.random(2)) + + # Create transformation class for spline + trafo = splinepy.helpme.integrate.Transformation(spline) + + # Check whether element quadrature points lie inside element + ukvs = spline.unique_knots + trafo.compute_all_element_quad_points() + # Check quadrature points of each element + for element_id, quad_points in enumerate(trafo.all_quad_points): + grid_id = trafo.get_element_grid_id(element_id) + # Extract the corners of the current element + element_corners = [ + ukv[grid_dim_id : grid_dim_id + 2] + for ukv, grid_dim_id in zip(ukvs, grid_id) + ] + # Check if quadrature points lie within element + for dim, corners in enumerate(element_corners): + assert np.all( + (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) + ), f"Quadrature points do not lie within element for dimension {dim}" + + # For given spline, all Jacobians should be identity matrix + eye = np.eye(spline.para_dim) + trafo.compute_all_element_jacobian_inverses() + for element_jacobians in trafo.all_jacobians: + for jacobian_at_quad_point in element_jacobians: + assert np.allclose(eye, jacobian_at_quad_point), ( + "All Jacobians should be identity matrix" + ) + + # For created spline, all determinants should equal one + trafo.compute_all_element_jacobian_determinants() + assert np.allclose( + trafo.all_jacobian_determinants, + np.ones_like(trafo.all_jacobian_determinants), + ), "All Jacobians' determinants should be equal to one" From 87834b9279f962da5524797ebd7918efaeefc68c Mon Sep 17 00:00:00 2001 From: markriegler Date: Fri, 6 Sep 2024 15:21:27 +0200 Subject: [PATCH 162/171] Remove formula from Dirichlet BC docstring --- splinepy/helpme/integrate.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 670acbf5c..758c12bdd 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -1117,21 +1117,10 @@ def apply_dirichlet_boundary_conditions( west=False, ): """ - Applies Dirichlet boundary conditions via :math:`L^2`-projection. + Applies Dirichlet boundary conditions via L2-projection. - For a given function g, :math:`L^2`-projection is obtained by solving - the following equation: - - .. math:: \\sum\\limits_{j=1}^n \\alpha_j (N_i, N_j) = (g, N_i) \\quad - \forall i = 1, \\dots, n. - - Then, :math:`Pg`, the :math:`L^2`-projection of the function, is - given by - - .. math:: Pg(x) = \\sum\\limits_{j=1}^n \\alpha_j \\phi_j(x). - - For the Dirichlet boundary conditions, only the DoFs corresponding to - the boundaries are taken from the :math:`L^2`-projection. + Firstly, the function is L2-projected on the whole domain, but only the DoFs + corresponding to the boundaries are taken into account. Parameters ------------- From fd853980d12124e5855fefa1032bbb6102fbcc84 Mon Sep 17 00:00:00 2001 From: markriegler Date: Tue, 3 Jun 2025 10:52:54 +0200 Subject: [PATCH 163/171] Add comments for more clarity --- ...lerkin_laplace_problem_field_integrator.py | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/examples/iga/galerkin_laplace_problem_field_integrator.py b/examples/iga/galerkin_laplace_problem_field_integrator.py index e6dff58b0..a9ac952d8 100644 --- a/examples/iga/galerkin_laplace_problem_field_integrator.py +++ b/examples/iga/galerkin_laplace_problem_field_integrator.py @@ -5,8 +5,9 @@ -\Delta u = f with source term f=1. -First, homogeneous Dirichlet boundary conditions are applied. Then, homogeneous -and inhomogeneous Dirichlet boundary conditions are applied on 3 of 4 boundaries. +First, homogeneous Dirichlet boundary conditions are applied. Then, a mix of homogeneous +and inhomogeneous Dirichlet boundary conditions are applied on 3 of 4 boundaries. A +zero Neumann boundary condition is implicitly applied on the fourth boundary. """ import numpy as np @@ -18,6 +19,17 @@ def prepare_geometry_and_solution_field(): + """ + Creates single patch geometry and solution field with h- and p-refinement + + Returns + ----------------- + geometry: splinepy.BSpline + The geometry object + solution_field: splinepy.BSpline + The solution field with refinements. Its control points serve as the degree of + freedoms (DoFs) of the solution. It is initialized to a vector of ones. + """ # Define the Geometry geometry = sp.BSpline( degrees=[2, 2], @@ -37,12 +49,14 @@ def prepare_geometry_and_solution_field(): # Define the solution field solution_field = geometry.copy() + # Initialize solution vector solution_field.control_points = np.ones( (geometry.control_points.shape[0], 1) ) - # Use refinement + # Apply p-refinement solution_field.elevate_degrees([0, 1, 0, 1]) + # Apply uniform h-refinement new_knots = np.linspace(1 / n_refine, 1, n_refine, endpoint=False) solution_field.insert_knots(0, new_knots) solution_field.insert_knots(1, new_knots) @@ -120,22 +134,26 @@ def show_solution(geometry, solution_field): if __name__ == "__main__": + # Create geometry and solution field with h- and p-refinement geometry, solution_field = prepare_geometry_and_solution_field() fi = sp.helpme.integrate.FieldIntegrator( geometry=geometry, solution_field=solution_field ) + # Assemble the linear system for the Poisson equation fi.assemble_matrix(poisson_lhs) fi.assemble_vector(poisson_rhs) - # Homogeneous Dirichlet boundary conditions + # Apply homogeneous Dirichlet boundary conditions fi.apply_homogeneous_dirichlet_boundary_conditions() + # Solve linear system to obtain solution vector, which is stored as the + # solution_field's control points fi.solve_linear_system() # Plot geometry and field show_solution(geometry, solution_field) - # Inhomogeneous Dirichlet boundary conditions + # Function for inhomogeneous Dirichlet boundary conditions def dirichlet_function(points): """ On the boundary apply: g(x,y) = x/4 @@ -146,10 +164,14 @@ def dirichlet_function(points): fi.assemble_matrix(poisson_lhs) fi.assemble_vector(poisson_rhs) - # Apply boundary conditions on 3 boundaries + # Apply boundary conditions on 3 boundaries (west, south and north boundary) fi.apply_homogeneous_dirichlet_boundary_conditions(west=True) fi.apply_dirichlet_boundary_conditions( dirichlet_function, south=True, north=True ) + # For zero Neumann boundary conditions we can keep the matrix and rhs as they are + + # Solve linear system fi.solve_linear_system() + # Plot resulting solution show_solution(geometry, solution_field) From 6165eb92d249890e0e7ae6460955308c4c99e66e Mon Sep 17 00:00:00 2001 From: markriegler Date: Mon, 20 Apr 2026 16:13:15 +0200 Subject: [PATCH 164/171] Add docstring --- splinepy/helpme/integrate.py | 64 ++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 758c12bdd..59bf984e0 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -272,6 +272,22 @@ def parametric_function(self, *args, **kwargs): class Transformation: + """Helper class to be used for the numerical integration of field variables on a + single-patch geometry. + + It computes the elements within the patch and for each one of them the quadrature + points and the Jacobian at those points. + + Parameters + ---------- + spline: spline + The geometry + solution_field: None or spline + Solution field as spline function. If not given, supports and quadrature + will be calculated for geometry + orders: None or list + Quadrature orders. If not given, default quadrature orders will be used + """ __slots__ = ( "_spline", "_solution_field", @@ -291,19 +307,6 @@ class Transformation: ) def __init__(self, spline, solution_field=None, orders=None): - """ - Setup transformation class with a given geometry. - - Parameters - ------------ - spline: spline - The geometry - solution_field: None or spline - Solution field as spline function. If not given, supports and quadrature - will be calculated for geometry - orders: None or list - Quadrature orders. If not given, default quadrature orders will be used - """ self._spline = spline self._solution_field = solution_field if solution_field is not None: @@ -679,6 +682,26 @@ def compute_all_element_measures(self, recompute=False): class FieldIntegrator: + """ + Class for the numerical evaluation of a PDE on a single-patch geometry. + + On intialization it sets up solution field, its mapper, precomputes the + transformation and calculate the number of DoFs. + + + + Parameters + ---------- + geometry: spline + The geometry + solution_field: None or spline + Solution field. If not given, quadrature and supports will be calculated + using the geometry + orders: None or list + Quadrature order in each dimension. If not given, default quadrature + will be used + """ + __slots__ = ( "_helpee", "_solution_field", @@ -691,21 +714,6 @@ class FieldIntegrator: ) def __init__(self, geometry, solution_field=None, orders=None): - """ - Sets up solution field, its mapper, precomputes the transformation and - calculate the number of DoFs. - - Parameters - ---------- - geometry: spline - The geometry - solution_field: None or spline - Solution field. If not given, quadrature and supports will be calculated - using the geometry - orders: None or list - Quadrature order in each dimension. If not given, default quadrature - will be used - """ self._helpee = geometry if solution_field is None: self._solution_field = geometry.copy() From 3bb707105fa4b977f1096d1cb8f42b08569ae244 Mon Sep 17 00:00:00 2001 From: markriegler Date: Wed, 22 Apr 2026 17:55:34 +0200 Subject: [PATCH 165/171] Fix error with blackdoc --- splinepy/helpme/integrate.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 59bf984e0..4f92a2603 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -272,12 +272,12 @@ def parametric_function(self, *args, **kwargs): class Transformation: - """Helper class to be used for the numerical integration of field variables on a + """Helper class to be used for the numerical integration of field variables on a single-patch geometry. - + It computes the elements within the patch and for each one of them the quadrature points and the Jacobian at those points. - + Parameters ---------- spline: spline @@ -288,6 +288,7 @@ class Transformation: orders: None or list Quadrature orders. If not given, default quadrature orders will be used """ + __slots__ = ( "_spline", "_solution_field", @@ -684,11 +685,11 @@ def compute_all_element_measures(self, recompute=False): class FieldIntegrator: """ Class for the numerical evaluation of a PDE on a single-patch geometry. - - On intialization it sets up solution field, its mapper, precomputes the + + On initialization it sets up solution field, its mapper, precomputes the transformation and calculate the number of DoFs. - - + + Parameters ---------- @@ -701,7 +702,7 @@ class FieldIntegrator: Quadrature order in each dimension. If not given, default quadrature will be used """ - + __slots__ = ( "_helpee", "_solution_field", From 5bb1bb1bb109e653ae5ae229e060bc7519e47394 Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Tue, 12 May 2026 19:49:17 +0200 Subject: [PATCH 166/171] Format: missing file --- splinepy/helpme/integrate.py | 189 ++++++++++++++++++++++++++------- tests/helpme/test_integrate.py | 66 ++++++++++-- 2 files changed, 207 insertions(+), 48 deletions(-) diff --git a/splinepy/helpme/integrate.py b/splinepy/helpme/integrate.py index 4f92a2603..c2c3fd3af 100644 --- a/splinepy/helpme/integrate.py +++ b/splinepy/helpme/integrate.py @@ -49,7 +49,9 @@ def measure(spline_patch, positions): def measure(spline_patch, positions): jacs = spline_patch.jacobian(positions) - return _np.sqrt(_np.linalg.det(_np.einsum("oji,ojk->oik", jacs, jacs))) + return _np.sqrt( + _np.linalg.det(_np.einsum("oji,ojk->oik", jacs, jacs)) + ) return measure @@ -115,7 +117,9 @@ def _get_quadrature_information(spline, orders=None): else: quad_orders = _np.ascontiguousarray(orders, dtype=int).flatten() if quad_orders.size != spline.para_dim: - raise ValueError("Integration order must be array of size para_dim") + raise ValueError( + "Integration order must be array of size para_dim" + ) for order in quad_orders: # Get legendre quadratuer points @@ -162,7 +166,10 @@ def volume(spline, orders=None): # Calculate Volume if spline.has_knot_vectors: volume = _np.sum( - [_np.sum(meas(b, positions) * weights) for b in spline.extract.beziers()] + [ + _np.sum(meas(b, positions) * weights) + for b in spline.extract.beziers() + ] ) else: volume = _np.sum(meas(spline, positions) * weights) @@ -229,14 +236,70 @@ def parametric_function( def physical_function( - function, # noqa ARG001 - orders, # noqa ARG001 + spline, + function, + orders=None, ): - """Integrate a function defined within the physical domain""" - raise NotImplementedError( - "Function not implemented yet. Please feel free to write an issue, if " - "you need it: github.com/tatarata/splinepy/issues" - ) + """Integrate a function defined within the selected domain + + Parameters + ---------- + spline : Spline + The geometry over which the function is integrated + function : Callable + The user-defined function to integrate. + orders : optional + Quadrature order in parametric domain for numerical integration + + Returns + ------- + integral : np.ndarray + """ + from splinepy.spline import Spline as _Spline + + # Check input type + if not isinstance(spline, _Spline): + raise NotImplementedError("integration only works for splines") + + # Retrieve aux info + meas = _get_integral_measure(spline) + positions, weights = _get_quadrature_information(spline, orders) + + # define function to integrate + def _function(position): + return function(spline.evaluate(position)) + + # Calculate Volume + if spline.has_knot_vectors: + # positions must be mapped into each knot-element + para_view = spline.create.parametric_view(axes=False) + + # get initial shape + initial = function([positions[0]]) + result = _np.zeros(initial.shape[1]) + for bezier_element in para_view.extract.beziers(): + # Get the bezier element scaling factor to get the correct + # element size from original knot element + knot_element_scaling_factor = _np.prod( + _np.diff(bezier_element.control_point_bounds, axis=0) + ) + quad_positions = bezier_element.evaluate(positions) + result += _np.einsum( + "i...,i,i->...", + _function(quad_positions), + meas(spline, quad_positions) * knot_element_scaling_factor, + weights, + optimize=True, + ) + else: + result = _np.einsum( + "i...,i,i->...", + _function(positions), + meas(spline, positions), + weights, + optimize=True, + ) + return result class Integrator: @@ -270,6 +333,10 @@ def volume(self, *args, **kwargs): def parametric_function(self, *args, **kwargs): return parametric_function(self._helpee, *args, **kwargs) + @_wraps(physical_function) + def physical_function(self, *args, **kwargs): + return physical_function(self._helpee, *args, **kwargs) + class Transformation: """Helper class to be used for the numerical integration of field variables on a @@ -330,7 +397,9 @@ def __init__(self, spline, solution_field=None, orders=None): if orders is None: quad_positions = [] quad_weights = [] - for dim_quadrature_order in _default_quadrature_orders(spline_for_quad): + for dim_quadrature_order in _default_quadrature_orders( + spline_for_quad + ): quad_position, quad_weight = _np.polynomial.legendre.leggauss( deg=dim_quadrature_order ) @@ -340,10 +409,12 @@ def __init__(self, spline, solution_field=None, orders=None): quad_weights.append(quad_weight / 2) self._quad_positions = _cartesian_product(quad_positions) - self._quad_weights = _np.prod(_cartesian_product(quad_weights), axis=1) + self._quad_weights = _np.prod( + _cartesian_product(quad_weights), axis=1 + ) else: - self._quad_positions, self._quad_weights = _get_quadrature_information( - spline_for_quad, orders + self._quad_positions, self._quad_weights = ( + _get_quadrature_information(spline_for_quad, orders) ) # Precompute grid IDs @@ -421,7 +492,9 @@ def get_element_grid_id(self, element_id): The grid ID of the element. """ if self._para_dim == 3: - raise NotImplementedError("Element grid ID not yet implemented for 3D") + raise NotImplementedError( + "Element grid ID not yet implemented for 3D" + ) return self._grid_ids[element_id, :] @@ -448,7 +521,9 @@ def get_element_quad_points(self, element_id): element_corner_points = _np.vstack( [ ukv_dim[e_dim_id : (e_dim_id + 2)] - for ukv_dim, e_dim_id in zip(self._ukv, element_grid_id) + for ukv_dim, e_dim_id in zip( + self._ukv, element_grid_id, strict=True + ) ] ) element_lengths = _np.diff(element_corner_points, axis=1).ravel() @@ -523,7 +598,10 @@ def jacobian_inverse(self, element_id): element_jacobians = self.jacobian(element_id) element_jacobian_inverse = _np.stack( - [_np.linalg.inv(element_jacobian) for element_jacobian in element_jacobians] + [ + _np.linalg.inv(element_jacobian) + for element_jacobian in element_jacobians + ] ) return element_jacobian_inverse @@ -547,7 +625,10 @@ def jacobian_determinant(self, element_id): element_jacobians = self.jacobian(element_id) return _np.array( - [_np.linalg.det(element_jacobian) for element_jacobian in element_jacobians] + [ + _np.linalg.det(element_jacobian) + for element_jacobian in element_jacobians + ] ) def compute_all_element_quad_points(self, recompute=False): @@ -593,7 +674,9 @@ def compute_all_supports(self, recompute=False): self.compute_all_element_quad_points(recompute=recompute) relevant_spline = ( - self._spline if self._solution_field is None else self._solution_field + self._spline + if self._solution_field is None + else self._solution_field ) self._all_supports = [ relevant_spline.support(quad_points[:1, :]).ravel() @@ -718,7 +801,9 @@ def __init__(self, geometry, solution_field=None, orders=None): self._helpee = geometry if solution_field is None: self._solution_field = geometry.copy() - self._solution_field.control_points = _np.zeros((geometry.cps.shape[0], 1)) + self._solution_field.control_points = _np.zeros( + (geometry.cps.shape[0], 1) + ) else: self._solution_field = solution_field self._ndofs = int( @@ -728,6 +813,7 @@ def __init__(self, geometry, solution_field=None, orders=None): for deg, kv in zip( self._solution_field.degrees, self._solution_field.knot_vectors, + strict=True, ) ] ) @@ -745,7 +831,9 @@ def reset(self, orders=None): If given, these orders will be used for quadrature. Otherwise, default quadrature orders will be used. """ - self._trafo = Transformation(self._helpee, self._solution_field, orders) + self._trafo = Transformation( + self._helpee, self._solution_field, orders + ) self.precompute_transformation() self._supports = None @@ -795,14 +883,16 @@ def assemble_matrix(self, function, matrixout=None): # If other matrix is used, check if it compatible if matrixout is not None: if _has_scipy: - assert isinstance(matrixout, _dok_matrix), ( - "Matrixout must be scipy sparse dok matrix" - ) + assert isinstance( + matrixout, _dok_matrix + ), "Matrixout must be scipy sparse dok matrix" else: assert isinstance(matrixout, _np.ndarray) assert matrixout.shape == (self._ndofs, self._ndofs) - system_matrix = self._global_system_matrix if matrixout is None else matrixout + system_matrix = ( + self._global_system_matrix if matrixout is None else matrixout + ) quad_weights = self._trafo.quadrature_weights @@ -811,6 +901,7 @@ def assemble_matrix(self, function, matrixout=None): self._trafo.all_jacobian_determinants, self._trafo.all_supports, self._trafo.all_quad_points, + strict=True, ): element_matrix = function( mapper=self._mapper, @@ -863,6 +954,7 @@ def assemble_vector(self, function, current_sol=None, vectorout=None): self._trafo.all_jacobian_determinants, self._trafo.all_supports, self._trafo.all_quad_points, + strict=True, ): # Assemble element vector function_args["quad_points"] = element_quad_points @@ -903,15 +995,17 @@ def assemble_matrix_and_vector( # If other matrix is used, check if it compatible if matrixout is not None: if _has_scipy: - assert isinstance(matrixout, _dok_matrix), ( - "Matrixout must be scipy sparse dok matrix" - ) + assert isinstance( + matrixout, _dok_matrix + ), "Matrixout must be scipy sparse dok matrix" else: assert isinstance(matrixout, _np.ndarray) assert matrixout.shape == (self._ndofs, self._ndofs) # Set matrix accordingly - system_matrix = self._global_system_matrix if matrixout is None else matrixout + system_matrix = ( + self._global_system_matrix if matrixout is None else matrixout + ) # Initialize rhs vector if self._global_rhs is None: @@ -939,6 +1033,7 @@ def assemble_matrix_and_vector( self._trafo.all_jacobian_determinants, self._trafo.all_supports, self._trafo.all_quad_points, + strict=True, ): # Assemble element matrix and vector function_args["quad_points"] = element_quad_points @@ -972,7 +1067,9 @@ def L2_projection(self, function): L2-projected values for Dofs """ - def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): + def dirichlet_lhs_and_rhs( + mapper, quad_points, quad_weights, jacobian_det + ): # Assemble system matrix bf_values = mapper._field_reference.basis(quad_points) element_matrix = _np.einsum( @@ -985,7 +1082,9 @@ def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): ) # Assemble rhs - quad_points_forward = mapper._geometry_reference.evaluate(quad_points) + quad_points_forward = mapper._geometry_reference.evaluate( + quad_points + ) function_values = function(quad_points_forward) element_vector = _np.einsum( "qj,q,q,q->j", @@ -1000,7 +1099,9 @@ def dirichlet_lhs_and_rhs(mapper, quad_points, quad_weights, jacobian_det): # Initialiye mass matrix and rhs global_size = (self._ndofs, self._ndofs) - mass_matrix = _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) + mass_matrix = ( + _dok_matrix(global_size) if _has_scipy else _np.zeros(global_size) + ) rhs_vector = _np.zeros(self._ndofs) # Assemble lhs and rhs self.assemble_matrix_and_vector( @@ -1055,7 +1156,9 @@ def get_boundary_dofs( Indices of relevant boundary dofs """ relevant_spline = ( - self._helpee if self._solution_field is None else self._solution_field + self._helpee + if self._solution_field is None + else self._solution_field ) multi_index = relevant_spline.multi_index @@ -1071,13 +1174,17 @@ def get_boundary_dofs( if north or east or south or west: all_boundaries = False - boundaries = [True] * 4 if all_boundaries else [west, east, south, north] + boundaries = ( + [True] * 4 if all_boundaries else [west, east, south, north] + ) indices = _np.unique( _np.hstack( [ index - for index, boundary in zip(multi_indices, boundaries) + for index, boundary in zip( + multi_indices, boundaries, strict=True + ) if boundary ] ) @@ -1109,7 +1216,9 @@ def apply_homogeneous_dirichlet_boundary_conditions( """ self.check_if_assembled() - indices = self.get_boundary_dofs(all_boundaries, north, east, south, west) + indices = self.get_boundary_dofs( + all_boundaries, north, east, south, west + ) self._global_system_matrix[indices, :] = 0 self._global_system_matrix[indices, indices] = 1 @@ -1154,7 +1263,9 @@ def apply_dirichlet_boundary_conditions( dof_vector = self.L2_projection(function) # Get relevant dofs - indices = self.get_boundary_dofs(all_boundaries, north, east, south, west) + indices = self.get_boundary_dofs( + all_boundaries, north, east, south, west + ) if return_values: return dof_vector[indices], indices @@ -1219,7 +1330,9 @@ def compute_error(self, function, norm="l2"): # Integrate error quad_weights = self._trafo._quad_weights n_weights = quad_weights.shape[0] - quad_weights = _np.tile(quad_weights, len(solution_values) // len(quad_weights)) + quad_weights = _np.tile( + quad_weights, len(solution_values) // len(quad_weights) + ) self._trafo.compute_all_element_measures() error_integration = _np.einsum( diff --git a/tests/helpme/test_integrate.py b/tests/helpme/test_integrate.py index 85fb26a42..ee2444c7f 100644 --- a/tests/helpme/test_integrate.py +++ b/tests/helpme/test_integrate.py @@ -42,7 +42,9 @@ def test_volume_integration_embedded(np_rng): # For same curve, test other spline types assert np.allclose(bezier.bspline.integrate.volume(), expected_result) - assert np.allclose(bezier.rationalbezier.integrate.volume(), expected_result) + assert np.allclose( + bezier.rationalbezier.integrate.volume(), expected_result + ) assert np.allclose(bezier.nurbs.integrate.volume(), expected_result) # Check if volume is equal after degree elevation @@ -153,12 +155,18 @@ def test_complex_geometry(np_rng): def test_assertions(np_rng): """Test the assertions in volume function""" - bezier = splinepy.Bezier(degrees=[1, 2], control_points=np_rng.random((6, 1))) + bezier = splinepy.Bezier( + degrees=[1, 2], control_points=np_rng.random((6, 1)) + ) - with pytest.raises(ValueError, match=r"`Volume` not supported if para_dim > dim"): + with pytest.raises( + ValueError, match=r"`Volume` not supported if para_dim > dim" + ): bezier.integrate.volume() - bezier = splinepy.Bezier(degrees=[2, 2], control_points=np_rng.random((9, 2))) + bezier = splinepy.Bezier( + degrees=[2, 2], control_points=np_rng.random((9, 2)) + ) with pytest.raises( ValueError, match=r"Integration order must be array of size para_dim" @@ -176,7 +184,9 @@ def volume_function(x): vf[:, 1] = col1_factor return vf - bezier = splinepy.Bezier(degrees=[1, 2], control_points=np_rng.random((6, 2))) + bezier = splinepy.Bezier( + degrees=[1, 2], control_points=np_rng.random((6, 2)) + ) # Test function integration for constant functions assert np.allclose( [bezier.integrate.volume(), col1_factor * bezier.integrate.volume()], @@ -217,12 +227,13 @@ def test_transformation_class(np_rng): # Extract the corners of the current element element_corners = [ ukv[grid_dim_id : grid_dim_id + 2] - for ukv, grid_dim_id in zip(ukvs, grid_id) + for ukv, grid_dim_id in zip(ukvs, grid_id, strict=True) ] # Check if quadrature points lie within element for dim, corners in enumerate(element_corners): assert np.all( - (quad_points[:, dim] > corners[0]) & (quad_points[:, dim] < corners[1]) + (quad_points[:, dim] > corners[0]) + & (quad_points[:, dim] < corners[1]) ), f"Quadrature points do not lie within element for dimension {dim}" # For given spline, all Jacobians should be identity matrix @@ -230,9 +241,9 @@ def test_transformation_class(np_rng): trafo.compute_all_element_jacobian_inverses() for element_jacobians in trafo.all_jacobians: for jacobian_at_quad_point in element_jacobians: - assert np.allclose(eye, jacobian_at_quad_point), ( - "All Jacobians should be identity matrix" - ) + assert np.allclose( + eye, jacobian_at_quad_point + ), "All Jacobians should be identity matrix" # For created spline, all determinants should equal one trafo.compute_all_element_jacobian_determinants() @@ -240,3 +251,38 @@ def test_transformation_class(np_rng): trafo.all_jacobian_determinants, np.ones_like(trafo.all_jacobian_determinants), ), "All Jacobians' determinants should be equal to one" + + +def test_physical_function_integration(np_rng): + # Analytical integral of y*(5-y) over [0,1]x[0,5] rectangle + integral_analytical = 125 / 6 + + def parabolic_function(points): + if isinstance(points, list): + y = points[0][1] + elif isinstance(points, np.ndarray): + y = points[:, 1] + else: + raise TypeError("Unsupported type for points") + return (y * (5 - y)).reshape(-1, 1) + + # Create rectangle domain + xlin, ylin = np.linspace(0, 1, 3), np.linspace(0, 5, 3) + control_points = np.vstack( + [array.ravel() for array in np.meshgrid(xlin, ylin)] + ).T + rectangle = splinepy.BSpline( + degrees=[2, 2], + knot_vectors=[[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], + control_points=control_points, + ) + + # Insert random knots along both parametric dimensions + rectangle.insert_knots(0, np_rng.random(2)) + rectangle.insert_knots(1, np_rng.random(2)) + + integral = splinepy.helpme.integrate.physical_function( + rectangle, parabolic_function + ) + + assert np.allclose(integral, integral_analytical) From e983182ab9d470bf1fbd3a23b9911a434eafeda7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:25:21 +0000 Subject: [PATCH 167/171] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black-pre-commit-mirror: 26.3.1 → 26.5.1](https://github.com/psf/black-pre-commit-mirror/compare/26.3.1...26.5.1) - [github.com/astral-sh/ruff-pre-commit: v0.15.7 → v0.15.16](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.7...v0.15.16) - [github.com/pre-commit/mirrors-clang-format: v22.1.1 → v22.1.5](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.1...v22.1.5) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e9b8be63..8ec92220f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,19 +31,19 @@ repos: - id: trailing-whitespace - repo: https://github.com/psf/black-pre-commit-mirror - rev: "26.3.1" + rev: "26.5.1" hooks: - id: black additional_dependencies: [tomli] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.10 + rev: v0.15.16 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v22.1.3 + rev: v22.1.5 hooks: - id: clang-format types_or: [c, c++] From 18118c97f39fb464e98ec44a2fb2e859bf586885 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:40:11 +0000 Subject: [PATCH 168/171] Add detailed CHANGELOG.md for v0.2.1 --- CHANGELOG.md | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..9cad5d1f5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,157 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] - v0.2.1 + +### Added + +#### Swept Surface/Solid Generation +- New `splinepy.helpme.create.swept()` function for generating swept surfaces and solids by sweeping a cross-section along a trajectory spline ([examples/show_swept.py](examples/show_swept.py)) + - Supports curve (1D) and surface (2D) cross-sections to produce surfaces and solids respectively + - Cross-section orientation uses the projection-normal method (Siltanen & Woodward), following [The NURBS Book](https://link.springer.com/book/10.1007/978-3-642-59223-2), Piegl & Tiller, 2nd ed., Chapter 10.4 + - Supports closed trajectories with orientation correction (p. 483 of the NURBS book) + - `anchor` parameter controls which reference point of the cross-section is placed on the trajectory (`"parametric"`, `"control_box"`, `"geometry_box"`, or `"auto"`) + - `set_on_trajectory` parameter selects between placement at trajectory knot evaluation points or trajectory control points + - Optional `rotation_adaption` parameter for additional rotation of the cross-section around the trajectory tangent + - Added swept surface image to README and documentation + +#### IGA Assembly: `FieldIntegrator` and `Transformation` +- New `Transformation` class in `splinepy.helpme.integrate` encapsulating all quadrature and geometry transformation data needed for IGA discretization: + - Precomputes quadrature points, supports, Jacobians, Jacobian inverses, and Jacobian determinants per element + - Configurable quadrature orders +- New `FieldIntegrator` class in `splinepy.helpme.integrate` providing a high-level interface for PDE assembly: + - `assemble_matrix(function)` — assembles the global stiffness matrix + - `assemble_vector(function)` — assembles the global load vector + - `assemble_matrix_and_vector(function)` — assembles both simultaneously + - `L2_projection(function)` — computes the L2 projection of a function onto the spline space + - `compute_error(function, norm)` — computes the L2 or H1 error of the current solution + - `apply_homogeneous_dirichlet_boundary_conditions()` — enforces homogeneous Dirichlet BCs + - `apply_dirichlet_boundary_conditions(function)` — enforces inhomogeneous Dirichlet BCs from a given function + - `solve_linear_system()` — solves the assembled linear system using `scipy.sparse.linalg.spsolve` + - Uses sparse matrix assembly via `scipy.sparse.dok_matrix` + +#### Microstructure: Tile Parameter Handling and Sensitivities +- Extended `TileBase` with standardised class attributes and properties for parameter validation and introspection: + - `_parameter_bounds` / `parameter_bounds` — bounds for each tile parameter + - `_parameters_shape` / `parameters_shape` — shape of the parameter array + - `_default_parameter_value` / `default_parameter_value` — default values for tile parameters + - `_sensitivities_implemented` / `sensitivities_implemented` — flag indicating whether parameter sensitivities are implemented + - `_closure_directions` / `closure_directions` — list of supported closure directions + - `check_params(parameters)` — validates user-supplied parameters against bounds + - `_process_input(parameters, parameter_sensitivities)` — shared input-processing logic + - `_check_custom_parameter(value, param_name, bounds)` — validates individual parameters +- Derived tile classes now inherit common parameter-processing code from `TileBase`, reducing code duplication +- Changed `evaluation_points`, `para_dim`, and `dim` from class methods to instance properties for consistency +- Added `n_info_per_eval_point` property to `TileBase` +- Derivatives (parameter sensitivities) implemented or fixed for: + - `HollowOctagon` (with and without closure) + - `HollowOctagonExtrude` (added working closure, with derivatives) + - `Armadillo` + - `Chi` tile (fixed) + - `InverseCross3D` (restructured and fixed) + - `CubeVoid` (fixed) + - `EllipsVoid` (fixed) +- Fourth-order accurate finite-difference calculation for numerical verification of parameter sensitivities +- Sensitivities calculation now outputs evaluation points when it fails for easier debugging +- `Microstructure`: integers are now accepted as tile parameters alongside floats + +#### Multipatch +- Added `Multipatch.set_interface_orientations(interface_orientations)` setter for manually specifying interface orientations, enabling export of scalar-field splines where automatic Jacobian-based orientation computation is not possible + +#### Examples +- Added `examples/show_swept.py` demonstrating swept surface and solid construction +- Added `examples/iga/galerkin_laplace_problem_field_integrator.py` demonstrating Galerkin IGA for the Laplace problem using `FieldIntegrator` +- Added `examples/iga/collocation_stokes_problem_sparse.py` demonstrating IGA collocation for the Stokes problem using sparse matrices + +### Changed + +#### CI/CD and Python Support +- Dropped Python 3.9 support; added Python 3.14 support (updated workflows and `pyproject.toml` classifiers) +- Switched macOS CI runner from deprecated `macos-13` to `macos-15-intel` for continued x86_64 testing +- Fixed wheel naming convention issue in CI + +#### Microstructure +- Refactored all tile classes to use common functions from `TileBase`, reducing code duplication significantly +- `TileBase`: removed class methods in favour of instance/class properties for `dim`, `para_dim`, and `evaluation_points` +- `TileBase`: removed unneeded classmethods to simplify the class hierarchy +- `Microstructure`: replaced bare `assert` statements with proper `raise` expressions for parameter validation; improved error messages +- `Microstructure`: updated `zip` calls to use `strict=True` for stricter length checking (Python 3.10+) +- `Chi` tile is now excluded from macro-sensitivity tests (analytical sensitivities not yet finalized) +- `HollowOctagonExtrude` removed from the list of skipped test tiles + +#### Integration (`helpme/integrate.py`) +- Extracted `_default_quadrature_orders(spline)` helper function (previously inlined in `_get_quadrature_information`) +- Improved docstrings for `_get_integral_measure`, `_get_quadrature_information`, and related functions +- Corrected typos in docstrings (`paramtric` → `parametric`, `Determinante` → `Determinant`) + +#### Gismo IO (`splinepy/io/gismo.py`) +- Added helper functions for Gismo export +- Fixed patch range text format for multi-patch exports + +#### IRIT IO (`splinepy/io/irit.py`) +- Updated `zip` calls to use `strict=True` + +#### MFEM IO (`splinepy/io/mfem.py`) +- Fixed test data for Cartesian 2D and 3D meshes + +#### PR Template +- Updated pull request template to use `develop` as the default base branch + +### Fixed + +- **`helpme/create.py`**: Fixed error in rotation matrix calculation for e2 entries in swept surface generation; improved input validation and error messages throughout +- **`helpme/integrate.py`**: Fixed typos and improved docstrings +- **`microstructure/tiles`**: Fixed incorrect ordering in finite-difference sensitivity calculation; fixed derivative implementations for `Chi`, `CubeVoid`, `EllipsVoid`, `InverseCross3D`; fixed typo causing errors in `HollowOctagonExtrude` +- **`microstructure/microstructure.py`**: Fixed typo in class docstring (`facilitatae` → `facilitate`) +- **`multipatch.py`**: Added check to verify that Jacobians exist before computing interface orientations +- **`bspline.py`**: Allow any negative number in the knot vector (previously restricted), fixing issue [#476](https://github.com/tataratat/splinepy/pull/476) +- **`utils/data.py`**: Minor fixes +- **Various IO modules**: Fixed miscellaneous issues and improved error handling + +### Tests + +- Added `tests/test_microtiles.py` with comprehensive parametrised tests for all microtile classes: + - All tiles lie within the unit cube for default parameters + - Tiles with non-default parameters + - Closure tests + - Tile derivative (sensitivity) tests, including closure derivatives + - Macro-sensitivity tests (finite-difference verification) +- Added `tests/test_microstructure.py` with additional microstructure integration tests +- Added test for `Transformation` class in `tests/helpme/test_integrate.py` +- Extended `tests/helpme/test_create.py` with swept surface tests, including: + - Comparison of swept vs extruded surfaces + - Custom cross-section normal vector + - Anchor modes + - Derivative checks +- Fixed RNG seed, sensitivity step size, and number of random test points for reproducible results +- Updated various existing tests for compatibility with Python 3.10+ `zip(strict=True)` changes + +## [0.2.0] - 2025-06-01 + +- Physical function integration in `helpme/integrate.py` +- Gismo export helper functions +- Python 3.13 support +- Various bug fixes and CI/CD maintenance + +## [0.1.3] - 2024 + +- Proximity / nearest-point query updates + +## [0.1.2] - 2024 + +- MFEM 3D single-patch IO +- Padding initializer for splines + +## [0.1.1] - 2024 + +- Initial stable release series + +[Unreleased]: https://github.com/isosuite/splinepy/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/isosuite/splinepy/compare/v0.1.3...v0.2.0 +[0.1.3]: https://github.com/isosuite/splinepy/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/isosuite/splinepy/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/isosuite/splinepy/releases/tag/v0.1.1 From fa16dad086c01da3d6430d087d383d89a9fc1e74 Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Thu, 11 Jun 2026 16:59:05 +0200 Subject: [PATCH 169/171] Chore: Bump version, Swap tataratat legacy links to isosuite (why jae why?) lower reference python version for ruff --- README.md | 64 ++++++++++++++++++++++---------------------- pyproject.toml | 2 +- splinepy/_version.py | 2 +- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7d27b120b..3e3f46ad6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ ![txt_logo](docs/source/_static/splinepy_name.png) **splinepy - Library for prototyping spline geometries of arbitrary dimensions and degrees, and IGA** -[![workflow](https://github.com/tataratat/splinepy/actions/workflows/main.yml/badge.svg)](https://github.com/tataratat/splinepy/actions) +[![workflow](https://github.com/isosuite/splinepy/actions/workflows/main.yml/badge.svg)](https://github.com/isosuite/splinepy/actions) [![PyPI version](https://badge.fury.io/py/splinepy.svg)](https://badge.fury.io/py/splinepy) -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/tataratat/try-splinepy/main) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/isosuite/try-splinepy/main) ![gallery](docs/source/_static/gallery.png) @@ -19,7 +19,7 @@ pip install splinepy You can install it directly from the source: ```bash -git clone git@github.com:tataratat/splinepy.git +git clone git@github.com:isosuite/splinepy.git cd splinepy git submodule update --init --recursive pip install -e . @@ -27,15 +27,15 @@ pip install -e . ## Documentation Here are links to related documentation for the library: -- [Documentation home](https://tataratat.github.io/splinepy) -- [Introduction to splines](https://tataratat.github.io/splinepy/spline_intro.html#introduction-to-splines) -- [Spline visualization](https://tataratat.github.io/splinepy/spline_intro.html#visualizing-splines) +- [Documentation home](https://isosuite.github.io/splinepy) +- [Introduction to splines](https://isosuite.github.io/splinepy/spline_intro.html#introduction-to-splines) +- [Spline visualization](https://isosuite.github.io/splinepy/spline_intro.html#visualizing-splines) ## Quick start ### 1. Create a spline -Here, we will create a [NURBS](https://tataratat.github.io/splinepy/_generated/splinepy.nurbs.NURBS.html#splinepy.nurbs.NURBS) for the following example. Alternatively, we can also create [Bezier](https://tataratat.github.io/splinepy/_generated/splinepy.bezier.Bezier.html#splinepy.bezier.Bezier), [RationalBezier](https://tataratat.github.io/splinepy/_generated/splinepy.rational_bezier.RationalBezier.html#splinepy.rational_bezier.RationalBezier), and [BSpline](https://tataratat.github.io/splinepy/_generated/splinepy.bspline.BSpline.html#splinepy.bspline.BSpline). +Here, we will create a [NURBS](https://isosuite.github.io/splinepy/_generated/splinepy.nurbs.NURBS.html#splinepy.nurbs.NURBS) for the following example. Alternatively, we can also create [Bezier](https://isosuite.github.io/splinepy/_generated/splinepy.bezier.Bezier.html#splinepy.bezier.Bezier), [RationalBezier](https://isosuite.github.io/splinepy/_generated/splinepy.rational_bezier.RationalBezier.html#splinepy.rational_bezier.RationalBezier), and [BSpline](https://isosuite.github.io/splinepy/_generated/splinepy.bspline.BSpline.html#splinepy.bspline.BSpline). ```python import splinepy @@ -73,9 +73,9 @@ nurbs.show() ### 2. Modifications All the splines can be modified. For example, by 1. directly accessing properties, -2. [elevating degrees](https://tataratat.github.io/splinepy/_generated/splinepy.spline.Spline.elevate_degrees.html#splinepy.spline.Spline.elevate_degrees), -3. [inserting knots](https://tataratat.github.io/splinepy/_generated/splinepy.bspline.BSplineBase.insert_knots.html#splinepy.bspline.BSplineBase.insert_knots), -4. [reducing degrees](https://tataratat.github.io/splinepy/_generated/splinepy.bspline.BSplineBase.reduce_degrees.html) and [removing knots](https://tataratat.github.io/splinepy/_generated/splinepy.bspline.BSplineBase.remove_knots.html) with a specified tolerance +2. [elevating degrees](https://isosuite.github.io/splinepy/_generated/splinepy.spline.Spline.elevate_degrees.html#splinepy.spline.Spline.elevate_degrees), +3. [inserting knots](https://isosuite.github.io/splinepy/_generated/splinepy.bspline.BSplineBase.insert_knots.html#splinepy.bspline.BSplineBase.insert_knots), +4. [reducing degrees](https://isosuite.github.io/splinepy/_generated/splinepy.bspline.BSplineBase.reduce_degrees.html) and [removing knots](https://isosuite.github.io/splinepy/_generated/splinepy.bspline.BSplineBase.remove_knots.html) with a specified tolerance *Note: currently {3, 4} are limited to BSpline families.* ```python @@ -130,7 +130,7 @@ p_basis0 = nurbs.basis(queries, nthreads=2) splinepy.settings.NTHREADS = 3 p_basis1 = nurbs.basis(queries) ``` -We also implemented [point inversion](https://tataratat.github.io/splinepy/_generated/splinepy.spline.Spline.proximities.html#splinepy-spline-spline-proximities) for splines. +We also implemented [point inversion](https://isosuite.github.io/splinepy/_generated/splinepy.spline.Spline.proximities.html#splinepy-spline-spline-proximities) for splines. ```python # see docs for options para_coordinates = nurbs.proximities(physical_coordinates) @@ -153,11 +153,11 @@ In cases, where you may have to compute derivatives at the inverted locations or ``` ### 4. Helper Modules -There's a list of helper modules under the namespace `splinepy.helpme` to boost prototyping efficiencies. Please check out the full list [here](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.html)! +There's a list of helper modules under the namespace `splinepy.helpme` to boost prototyping efficiencies. Please check out the full list [here](https://isosuite.github.io/splinepy/_generated/splinepy.helpme.html)! Here are some highlights. #### 4.1 Create -[splinepy.helpme.create](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.create.html#module-splinepy.helpme.create) module can help you create several primitive shapes and another spline based on the existing spline. +[splinepy.helpme.create](https://isosuite.github.io/splinepy/_generated/splinepy.helpme.create.html#module-splinepy.helpme.create) module can help you create several primitive shapes and another spline based on the existing spline. ```python # basic splines box = splinepy.helpme.create.box(1, 2, 3) # length per dim @@ -171,7 +171,7 @@ torus = splinepy.helpme.create.torus( splinepy.show(["box", box], ["disk", disk], ["torus", torus]) ``` ![create_basic](docs/source/_static/readme_create_basic.png) -For the latter, you can directly access such functions through [spline.create](https://tataratat.github.io/splinepy/_generated/splinepy.spline.Spline.create.html#splinepy.spline.Spline.create). +For the latter, you can directly access such functions through [spline.create](https://isosuite.github.io/splinepy/_generated/splinepy.spline.Spline.create.html#splinepy.spline.Spline.create). ```python # based on existing splines extruded = nurbs.create.extruded(extrusion_vector=[1, 2, 3]) @@ -192,7 +192,7 @@ swept = splinepy.helpme.create.swept( ![show_swept](docs/source/_static/show_swept.png) ### 4.2 Extract -Using [splinepy.helpme.extract](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.extract.html#module-splinepy.helpme.extract) module, you can extract meshes (as a [gustaf](https://tataratat.github.io/gustaf/index.html) object) +Using [splinepy.helpme.extract](https://isosuite.github.io/splinepy/_generated/splinepy.helpme.extract.html#module-splinepy.helpme.extract) module, you can extract meshes (as a [gustaf](https://isosuite.github.io/gustaf/index.html) object) ```python # extract meshes as gustaf objects control_mesh = nurbs.extract.control_mesh() @@ -206,7 +206,7 @@ splinepy.show( ) ``` ![extract_mesh](docs/source/_static/readme_extract_mesh.png) -or part of splines from an existing spline using [spline.extract](https://tataratat.github.io/splinepy/_generated/splinepy.spline.Spline.extract.html#splinepy.spline.Spline.extract). +or part of splines from an existing spline using [spline.extract](https://isosuite.github.io/splinepy/_generated/splinepy.spline.Spline.extract.html#splinepy.spline.Spline.extract). ```python # extract splines boundaries = nurbs.extract.boundaries() @@ -234,7 +234,7 @@ splinepy.show( ![extract_spline](docs/source/_static/readme_extract_spline.png) #### 4.3 Free-form deformation -Together with mesh types of [gustaf](https://tataratat.github.io/gustaf), we can perform [free-form deformation](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.ffd.FFD.html) +Together with mesh types of [gustaf](https://isosuite.github.io/gustaf), we can perform [free-form deformation](https://isosuite.github.io/splinepy/_generated/splinepy.helpme.ffd.FFD.html) ```python import gustaf as gus @@ -256,7 +256,7 @@ deformed = ffd.mesh ![ffd](docs/source/_static/readme_ffd.png) #### 4.4 Fitting -You can [fit](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.fit.html#module-splinepy.helpme.fit) your point data using splines. +You can [fit](https://isosuite.github.io/splinepy/_generated/splinepy.helpme.fit.html#module-splinepy.helpme.fit) your point data using splines. ```python data = [ [-0.955, 0.293], @@ -287,8 +287,8 @@ splinepy.show( #### 4.5 Mapper -[Mapper](https://tataratat.github.io/splinepy/_generated/splinepy.helpme.mapper.Mapper.html#splinepy.helpme.mapper.Mapper) class is a geometric mapping helper that brings expression and derivatives into the physical domain. -This is especially useful for trying collocation methods. Here, we show how you can create a left hand side matrix for a laplace problem - see [this example](https://github.com/tataratat/splinepy/blob/main/examples/iga/collocation_laplace_problem_sparse.py) for a full solution: +[Mapper](https://isosuite.github.io/splinepy/_generated/splinepy.helpme.mapper.Mapper.html#splinepy.helpme.mapper.Mapper) class is a geometric mapping helper that brings expression and derivatives into the physical domain. +This is especially useful for trying collocation methods. Here, we show how you can create a left hand side matrix for a laplace problem - see [this example](https://github.com/isosuite/splinepy/blob/main/examples/iga/collocation_laplace_problem_sparse.py) for a full solution:

```python @@ -313,14 +313,14 @@ laplacian_matrix = splinepy.utils.data.make_matrix( ``` ### 5. Microstructure -(Rational) Bezier splines in splinepy are capable of [composition](https://tataratat.github.io/splinepy/_generated/splinepy.bezier.BezierBase.compose.html#splinepy.bezier.BezierBase.compose), where you can place a spline (inner spline/function) into another spline (outer spline/function) in an exact fashion. +(Rational) Bezier splines in splinepy are capable of [composition](https://isosuite.github.io/splinepy/_generated/splinepy.bezier.BezierBase.compose.html#splinepy.bezier.BezierBase.compose), where you can place a spline (inner spline/function) into another spline (outer spline/function) in an exact fashion. We can systematically perform this to create certain shapes that consist of multiple inner splines. -The resulting shapes are called [microstructure](https://tataratat.github.io/splinepy/_generated/splinepy.microstructure.microstructure.Microstructure.html#splinepy.microstructure.microstructure.Microstructure)s and the inner spline that serves as a basis shape is called [tile](https://tataratat.github.io/splinepy/_generated/splinepy.microstructure.tiles.tile_base.TileBase.html#splinepy.microstructure.tiles.tile_base.TileBase). +The resulting shapes are called [microstructure](https://isosuite.github.io/splinepy/_generated/splinepy.microstructure.microstructure.Microstructure.html#splinepy.microstructure.microstructure.Microstructure)s and the inner spline that serves as a basis shape is called [tile](https://isosuite.github.io/splinepy/_generated/splinepy.microstructure.tiles.tile_base.TileBase.html#splinepy.microstructure.tiles.tile_base.TileBase). splinepy has several tiles that are ready to use. -Implementations of available tiles can be found [here](https://tataratat.github.io/splinepy/_generated/splinepy.microstructure.tiles.html). -However, it is easier to access them through [module functions](https://tataratat.github.io/splinepy/_generated/splinepy.microstructure.tiles.html): +Implementations of available tiles can be found [here](https://isosuite.github.io/splinepy/_generated/splinepy.microstructure.tiles.html). +However, it is easier to access them through [module functions](https://isosuite.github.io/splinepy/_generated/splinepy.microstructure.tiles.html): ```python splinepy.microstructure.tiles.show() ``` @@ -352,12 +352,12 @@ generated = microstructure.create() ![microstructures](docs/source/_static/readme_microstructure.png) -Please take a look at [this example](https://github.com/tataratat/splinepy/blob/main/examples/show_microstructures.py) for a broad overview of what microstructures can do! +Please take a look at [this example](https://github.com/isosuite/splinepy/blob/main/examples/show_microstructures.py) for a broad overview of what microstructures can do! ### 6. Multipatch -In practice, including [Microstructure](https://tataratat.github.io/splinepy/_generated/splinepy.microstructure.microstructure.Microstructure.html#splinepy.microstructure.microstructure.Microstructure)s, it is common to work with multiple patches. -For that, we provide a [Multipatch](https://tataratat.github.io/splinepy/_generated/splinepy.multipatch.Multipatch.html#splinepy.multipatch.Multipatch) class, equipped with various useful functionalities: +In practice, including [Microstructure](https://isosuite.github.io/splinepy/_generated/splinepy.microstructure.microstructure.Microstructure.html#splinepy.microstructure.microstructure.Microstructure)s, it is common to work with multiple patches. +For that, we provide a [Multipatch](https://isosuite.github.io/splinepy/_generated/splinepy.multipatch.Multipatch.html#splinepy.multipatch.Multipatch) class, equipped with various useful functionalities: - patch interface identification - boundary patch identification - boundary assignment with various options @@ -389,7 +389,7 @@ splinepy.io.gismo.export("microstructure.xml", generated) ![multipatch](docs/source/_static/readme_multipatch.png) ### 7. Input/output and vector graphics -splinepy supports various [IO formats](https://tataratat.github.io/splinepy/_generated/splinepy.io.html). +splinepy supports various [IO formats](https://isosuite.github.io/splinepy/_generated/splinepy.io.html). Most notably, [gismo](https://github.com/gismo/gismo) and [mfem](https://github.com/mfem/mfem) formats allow a seamless transition to analysis. In addition splinepy is also able to import and export the `iges` format. Specifically, `Type 126` (B-Spline curve) and `Type 128` (B-Spline surface). ```python # export @@ -400,16 +400,16 @@ quarter_circle = splinepy.io.mfem.load("quarter_circle.mesh") ``` -[svg format](https://tataratat.github.io/splinepy/_generated/splinepy.io.svg.export.html#splinepy.io.svg.export) enables true vector graphic export which preserves the smoothness of splines for publications/documentation. Try to zoom in! +[svg format](https://isosuite.github.io/splinepy/_generated/splinepy.io.svg.export.html#splinepy.io.svg.export) enables true vector graphic export which preserves the smoothness of splines for publications/documentation. Try to zoom in! ```python splinepy.io.svg.export("nurbs.svg", nurbs) ```

## Try online -You can also try splinepy online by clicking the [Binder](https://mybinder.org/v2/gh/tataratat/try-splinepy/main) badge above! +You can also try splinepy online by clicking the [Binder](https://mybinder.org/v2/gh/isosuite/try-splinepy/main) badge above! ## Contributing splinepy welcomes any form of contributions! -Feel free to write us an [issue](https://github.com/tataratat/splinepy/issues) or start a [discussion](https://github.com/tataratat/splinepy/discussions). -Contribution guidelines can be found [here](https://tataratat.github.io/splinepy/CONTRIBUTING.html). +Feel free to write us an [issue](https://github.com/isosuite/splinepy/issues) or start a [discussion](https://github.com/isosuite/splinepy/discussions). +Contribution guidelines can be found [here](https://isosuite.github.io/splinepy/CONTRIBUTING.html). diff --git a/pyproject.toml b/pyproject.toml index a984dae74..5e1fddafd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ lint.select = [ "A", # flake8-builtins ] lint.fixable = ["ALL"] -target-version = "py312" +target-version = "py310" lint.ignore = [ "PLR2004", # TODO! "PLR0912", # Too many branches diff --git a/splinepy/_version.py b/splinepy/_version.py index d3ec452c3..3ced3581b 100644 --- a/splinepy/_version.py +++ b/splinepy/_version.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" From fc78aa8854e4192983ee9456d956aa8786985853 Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Tue, 16 Jun 2026 18:11:47 +0200 Subject: [PATCH 170/171] Chore: Further tataratat legacy links and Coder review comments --- .gitmodules | 6 +- .pre-commit-config.yaml | 6 -- docs/markdown/spline_plotting.md | 6 +- docs/source/handle_markdown.py | 51 ++--------- docs/source/index.rst | 2 +- examples/ipynb/notebook_showcase_k3d.ipynb | 4 +- pyproject.toml | 5 +- splinepy/helpme/create.py | 89 +++++-------------- splinepy/microstructure/microstructure.py | 65 ++++---------- .../microstructure/tiles/cross_3d_linear.py | 37 +++----- .../microstructure/tiles/hollow_octagon.py | 7 ++ .../tiles/hollow_octagon_extrude.py | 10 ++- splinepy/microstructure/tiles/tile_base.py | 31 +++---- 13 files changed, 99 insertions(+), 220 deletions(-) diff --git a/.gitmodules b/.gitmodules index 2d8a696ec..b5ddf721c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,10 +3,10 @@ url = git@github.com:pybind/pybind11.git [submodule "third_party/napf"] path = third_party/napf - url = git@github.com:tataratat/napf.git + url = git@github.com:isosuite/napf.git [submodule "third_party/BSplineLib"] path = third_party/BSplineLib - url = git@github.com:tataratat/BSplineLib.git + url = git@github.com:isosuite/BSplineLib.git [submodule "third_party/bezman"] path = third_party/bezman - url = git@github.com:tataratat/bezman.git + url = git@github.com:isosuite/bezman.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ec92220f..0be11c149 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,12 +30,6 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace -- repo: https://github.com/psf/black-pre-commit-mirror - rev: "26.5.1" - hooks: - - id: black - additional_dependencies: [tomli] - - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.16 hooks: diff --git a/docs/markdown/spline_plotting.md b/docs/markdown/spline_plotting.md index 513db8653..fc874055c 100644 --- a/docs/markdown/spline_plotting.md +++ b/docs/markdown/spline_plotting.md @@ -1,7 +1,7 @@ # Visualization One of the highlights of splinepy is that we can visualize splines. Most of the classes have their own `show()` function to visualize current state of each object. -Visualizations utilize mesh types and data structures of [gustaf](https://tataratat.github.io/gustaf/). +Visualizations utilize mesh types and data structures of [gustaf](https://isosuite.github.io/gustaf/). Then, actual rendering happens with [vedo](https://vedo.embl.es) - a powerful scientific analysis and visualization library - check them out for details! The following will give a brief introduction to spline visualization. @@ -63,7 +63,7 @@ nurbs.show() ![plotting_data](../source/_static/plotting_data.png) You can easily plot data on splines. Scalar data can be represented with a colormap and vector data can be represented with arrows. -You can take a look at [this example](https://github.com/tataratat/splinepy/blob/main/examples/show_spline_data.py) for detailed introduction. +You can take a look at [this example](https://github.com/isosuite/splinepy/blob/main/examples/show_spline_data.py) for detailed introduction. ```python # set data - we will plot self spline, which will plot coordinates nurbs.spline_data["coords"] = nurbs @@ -92,7 +92,7 @@ nurbs.show() # Nr. 3 ``` ## Examples -Take a look at the examples folder for more! +Take a look at the examples folder for more! ## Notebook plotting diff --git a/docs/source/handle_markdown.py b/docs/source/handle_markdown.py index 213aaa3c3..97bb569a6 100644 --- a/docs/source/handle_markdown.py +++ b/docs/source/handle_markdown.py @@ -48,8 +48,7 @@ def get_special_links(line: str) -> list[tuple[str, str]]: """ possible = [ - x[::-1] - for x in re.findall(r"", line) + x[::-1] for x in re.findall(r"", line) ] return possible or "" @@ -71,7 +70,7 @@ def get_github_path_from(link): _type_: _description_ """ return str(pathlib.Path(link).resolve()).replace( - repo_root, "https://raw.githubusercontent.com/tataratat/splinepy/main/" + repo_root, "https://raw.githubusercontent.com/isosuite/splinepy/main/" ) @@ -111,9 +110,7 @@ def get_common_parent(path1: str, path2: str) -> str: } -def process_file( - file: str, relative_links: bool = True, return_content: bool = False -): +def process_file(file: str, relative_links: bool = True, return_content: bool = False): """Process a markdown file. This function will process a markdown file. It will replace all relative @@ -148,9 +145,7 @@ def process_file( stacklevel=3, ) continue - if item[1].startswith( - ("http", "#") - ): # skip http links and anchors + if item[1].startswith(("http", "#")): # skip http links and anchors if "badge" in item[1]: continue line = line.replace( # noqa: PLW2901 @@ -171,17 +166,13 @@ def process_file( "See documentation for examples.", ) elif not relative_links: # generate links to github repo - new_path = get_github_path_from( - pathlib.Path(item[1]).resolve() - ) + new_path = get_github_path_from(pathlib.Path(item[1]).resolve()) else: # generate relative links common_sub_path, steps_back = get_common_parent( item[1], folder_to_save_to ) new_path = "../" * steps_back + str( - pathlib.Path(item[1]) - .resolve() - .relative_to(common_sub_path) + pathlib.Path(item[1]).resolve().relative_to(common_sub_path) ) line = line.replace(item[1], str(new_path)) # noqa: PLW2901 @@ -199,41 +190,19 @@ def process_file( if item[0].startswith("http"): # skip http links and anchors continue elif not relative_links: # generate links to github repo - new_path = get_github_path_from( - pathlib.Path(item[1]).resolve() - ) + new_path = get_github_path_from(pathlib.Path(item[1]).resolve()) else: # just link to static folder in docs new_path = "_static/" + str(pathlib.Path(item[1]).name) line = line.replace(item[1], str(new_path)) # noqa: PLW2901 content += f"{line}" - # super special links (just special images) that the sphinx markdown - # parser won't correctly handle since they are in html tags. - special_links = get_special_links(content) - for item in special_links: - if not item[0].strip(): - warnings.warn( - f"Empty link in `{file}`. Link name `{item[1]}` link path " - f"`{item[0]}`. Will ignore link.", - stacklevel=3, - ) - continue - if item[0].startswith("http"): # skip http links and anchors - continue - else: - # just link to static folder in docs - new_path = "_static/" + str(pathlib.Path(item[1]).name) - content = content.replace(item[1], str(new_path)) - os.chdir(original_cwd) if return_content: return content - with open( - os.path.join(folder_to_save_to, os.path.basename(file)), "w" - ) as f: + with open(os.path.join(folder_to_save_to, os.path.basename(file)), "w") as f: f.write(content) @@ -246,9 +215,7 @@ def prepare_file_for_PyPI(): Args: file (str): Path to the README file. """ - content = process_file( - "README.md", relative_links=False, return_content=True - ) + content = process_file("README.md", relative_links=False, return_content=True) with open("README.md", "w") as f: f.write(content) diff --git a/docs/source/index.rst b/docs/source/index.rst index 5941a0b6c..da6df798d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,7 +16,7 @@ Table of Contents :caption: Library :maxdepth: 1 - Github + Github Contributing API Reference diff --git a/examples/ipynb/notebook_showcase_k3d.ipynb b/examples/ipynb/notebook_showcase_k3d.ipynb index b576fe425..32e4a0f4f 100644 --- a/examples/ipynb/notebook_showcase_k3d.ipynb +++ b/examples/ipynb/notebook_showcase_k3d.ipynb @@ -192,9 +192,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Have you heard of spline composition? With this method, you can create spline based microstructures in exact fashion. This is one of the highlights of the `splinepy`. For more information, please take a look at splinepy's [docs](https://tataratat.github.io/splinepy/_generated/splinepy.bezier.BezierBase.compose.html#splinepy.bezier.BezierBase.compose)\n", + "Have you heard of spline composition? With this method, you can create spline based microstructures in exact fashion. This is one of the highlights of the `splinepy`. For more information, please take a look at splinepy's [docs](https://isosuite.github.io/splinepy/_generated/splinepy.bezier.BezierBase.compose.html#splinepy.bezier.BezierBase.compose)\n", "\n", - "Creating microstructures require two ingredients: outer spline (also known as deformation function, outer function, ...) and a microtile. For this example, we will use empty torus as outer spline and 2d cross as microtile (see other available ready-to-use microtiles [here](https://tataratat.github.io/splinepy/_generated/splinepy.microstructure.tiles.html))." + "Creating microstructures require two ingredients: outer spline (also known as deformation function, outer function, ...) and a microtile. For this example, we will use empty torus as outer spline and 2d cross as microtile (see other available ready-to-use microtiles [here](https://isosuite.github.io/splinepy/_generated/splinepy.microstructure.tiles.html))." ] }, { diff --git a/pyproject.toml b/pyproject.toml index 5e1fddafd..9590bd36c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ license = {file = "LICENSE"} description = "Library for prototyping spline geometries of arbitrary dimensions and degrees, and IGA" keywords = ["bezier", "rational bezier", "bspline", "nurbs", "multi patch"] -urls = {Homepage = "https://github.com/tataratat/splinepy"} +urls = {Homepage = "https://github.com/isosuite/splinepy"} classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", @@ -113,9 +113,6 @@ lint.ignore = [ ignore-words-list = "connec,tye,ned" skip="./docs/source/_generated/**,./docs/build/*,./build/*,./third_party/*,./tests/data/*.svg,*.html,*.js" -[tool.black] -line-length = 79 -exclude = "third_party" [tool.blackdoc] line-length = 75 diff --git a/splinepy/helpme/create.py b/splinepy/helpme/create.py index fa407caaa..7ba756846 100644 --- a/splinepy/helpme/create.py +++ b/splinepy/helpme/create.py @@ -36,9 +36,7 @@ def embedded(spline, new_dimension): if spline.dim > new_dimension: spline_dict = spline.todict() - spline_dict["control_points"] = spline_dict["control_points"][ - :, :new_dimension - ] + spline_dict["control_points"] = spline_dict["control_points"][:, :new_dimension] return type(spline)(**spline_dict) elif spline.dim < new_dimension: spline_dict = spline.todict() @@ -105,9 +103,7 @@ def extruded(spline, extrusion_vector=None): ) ) else: - raise ValueError( - "Dimension Mismatch between extrusion extrusion vector and spline." - ) + raise ValueError("Dimension Mismatch between extrusion vector and spline.") # Start Extrusion spline_dict = {} @@ -117,9 +113,7 @@ def extruded(spline, extrusion_vector=None): if spline.has_knot_vectors: spline_dict["knot_vectors"] = spline.knot_vectors + [[0, 0, 1, 1]] if spline.is_rational: - spline_dict["weights"] = _np.concatenate( - (spline.weights, spline.weights) - ) + spline_dict["weights"] = _np.concatenate((spline.weights, spline.weights)) return type(spline)(**spline_dict) @@ -160,9 +154,7 @@ def revolved( axis = _np.asarray(axis).ravel() # Check Axis dimension if spline.control_points.shape[1] > axis.shape[0]: - raise ValueError( - "Dimension Mismatch between extrusion axis and spline." - ) + raise ValueError("Dimension Mismatch between extrusion axis and spline.") elif spline.control_points.shape[1] < axis.shape[0]: _log.debug( "Control Point dimension is smaller than axis dimension," @@ -172,9 +164,7 @@ def revolved( cps = _np.hstack( ( spline.control_points, - _np.zeros( - (len(spline.control_points), expansion_dimension) - ), + _np.zeros((len(spline.control_points), expansion_dimension)), ) ) else: @@ -212,24 +202,20 @@ def revolved( center = _np.asarray(center).ravel() # Check Axis dimension if not (problem_dimension == center.shape[0]): - raise ValueError( - "Dimension Mismatch between axis and center of rotation." - ) + raise ValueError("Dimension Mismatch between axis and center of rotation.") cps -= center # The parametric dimension is independent of the revolution but the # rotation-matrix is only implemented for 2D and 3D problems if cps.shape[1] not in {2, 3}: raise NotImplementedError( - "Sorry,revolutions only implemented for 2D and 3D splines" + "Sorry, revolutions only implemented for 2D and 3D splines" ) # Angle must be (0, pi) non including # Rotation is always performed in half steps PI = _np.pi - minimum_n_knot_spans = int( - _np.ceil((abs(angle) + abs(_settings.TOLERANCE)) / PI) - ) + minimum_n_knot_spans = int(_np.ceil((abs(angle) + abs(_settings.TOLERANCE)) / PI)) if n_knot_spans is None or (n_knot_spans < minimum_n_knot_spans): n_knot_spans = minimum_n_knot_spans @@ -275,9 +261,7 @@ def revolved( if spline.has_knot_vectors: kv = [0, 0, 0] [kv.extend([i + 1, i + 1]) for i in range(n_knot_spans - 1)] - spline_dict["knot_vectors"] = spline.knot_vectors + [ - kv + [n_knot_spans] * 3 - ] + spline_dict["knot_vectors"] = spline.knot_vectors + [kv + [n_knot_spans] * 3] if spline.is_rational: mid_weights = spline.weights * weight spline_dict["weights"] = spline.weights @@ -383,24 +367,16 @@ def swept( if isinstance(cross_section, Spline) and isinstance(trajectory, Spline): if not isinstance(cross_section, (_BSpline, _NURBS)): - raise TypeError( - "cross_section must be an instance of BSpline or NURBS" - ) + raise TypeError("cross_section must be an instance of BSpline or NURBS") if not isinstance(trajectory, (_BSpline, _NURBS)): - raise TypeError( - "trajectory must be an instance of BSpline or NURBS" - ) + raise TypeError("trajectory must be an instance of BSpline or NURBS") else: - raise TypeError( - "cross_section and trajectory must be instances of Spline" - ) + raise TypeError("cross_section and trajectory must be instances of Spline") if not trajectory.para_dim == 1: raise ValueError("trajectory must have a parametric dimension of 1") if cross_section.para_dim > 2: - raise ValueError( - "cross_section must have a parametric dimension of at most 2" - ) + raise ValueError("cross_section must have a parametric dimension of at most 2") if not isinstance(set_on_trajectory, bool): raise TypeError("set_on_trajectory must be a boolean") @@ -409,9 +385,7 @@ def swept( try: rotation_adaption = float(rotation_adaption) except TypeError: - raise TypeError( - "rotation_adaption must be a number (float, int) or None" - ) + raise TypeError("rotation_adaption must be a number (float, int) or None") if cross_section_normal is None: cross_section_normal = _np.array([0, 0, 1]) @@ -421,21 +395,15 @@ def swept( try: cross_section_normal = _np.asarray(cross_section_normal).ravel() except (TypeError, ValueError): - raise TypeError( - "cross_section_normal must be array-like and a 3D vector" - ) + raise TypeError("cross_section_normal must be array-like and a 3D vector") if cross_section_normal.shape != (3,): - raise ValueError( - "cross_section_normal must be array-like and a 3D vector" - ) + raise ValueError("cross_section_normal must be array-like and a 3D vector") if not isinstance(anchor, str): raise TypeError("anchor must be a string") anchor = anchor.lower() if anchor == "auto": - anchor = ( - "geometry_box" if cross_section.para_dim == 1 else "parametric" - ) + anchor = "geometry_box" if cross_section.para_dim == 1 else "parametric" if anchor not in {"parametric", "control_box", "geometry_box"}: raise ValueError( "anchor must be one of 'auto', 'parametric', " @@ -553,8 +521,7 @@ def swept( # calculation according to NURBS Book, eq. 10.27 for i in range(len(par_value)): B_rec[i + 1] = ( - B_rec[i] - - _np.dot(B_rec[i], tang_collection[i]) * tang_collection[i] + B_rec[i] - _np.dot(B_rec[i], tang_collection[i]) * tang_collection[i] ) if _np.linalg.norm(B_rec[i + 1]) < _settings.TOLERANCE: _log.warning( @@ -640,13 +607,9 @@ def swept( cs_center = 0.5 * (cs_min + cs_max) else: # geometry_box if cross_section.para_dim == 1: - sample_resolution = max( - 101, 4 * cross_section.control_points.shape[0] - ) + sample_resolution = max(101, 4 * cross_section.control_points.shape[0]) else: - sample_resolution = int( - _np.ceil(625 ** (1 / cross_section.para_dim)) - ) + sample_resolution = int(_np.ceil(625 ** (1 / cross_section.para_dim))) sample_resolution = max(5, min(25, sample_resolution)) sample_axes = [ @@ -678,9 +641,7 @@ def swept( else: section_origin = trajectory.control_points[i] - swept_spline_cps.append( - rotated_cross_section_cps @ A[i].T + section_origin - ) + swept_spline_cps.append(rotated_cross_section_cps @ A[i].T + section_origin) # create spline dictionary dict_swept_spline = { @@ -689,9 +650,7 @@ def swept( *cross_section.knot_vectors, *trajectory.knot_vectors, ], - "control_points": _np.asarray(swept_spline_cps).reshape( - -1, cross_section.dim - ), + "control_points": _np.asarray(swept_spline_cps).reshape(-1, cross_section.dim), } # add weights properly if spline is rational @@ -900,9 +859,7 @@ def parametric_view(spline, axes=True, conform=False): para_spline: BSpline """ p_bounds = spline.parametric_bounds - para_spline = from_bounds( - parametric_bounds=p_bounds, physical_bounds=p_bounds - ) + para_spline = from_bounds(parametric_bounds=p_bounds, physical_bounds=p_bounds) # process to create conforming para_view splines if conform: diff --git a/splinepy/microstructure/microstructure.py b/splinepy/microstructure/microstructure.py index 8bf9b79f0..ec3424750 100644 --- a/splinepy/microstructure/microstructure.py +++ b/splinepy/microstructure/microstructure.py @@ -119,9 +119,7 @@ def tiling(self, tiling): None """ if not isinstance(tiling, list) and not isinstance(tiling, int): - raise ValueError( - "Tiling mus be either list of integers of integer value" - ) + raise ValueError("Tiling mus be either list of integers of integer value") self._tiling = tiling # Is defaulted to False using function arguments self._sanity_check() @@ -275,9 +273,7 @@ def _additional_knots(self, knot_span_wise): "New knots will be inserted one by one with the objective" " to evenly distribute tiles within the parametric domain" ) - for i_pd, (tt, ukv) in enumerate( - zip(self.tiling, ukvs, strict=True) - ): + for i_pd, (tt, ukv) in enumerate(zip(self.tiling, ukvs, strict=True)): n_current_spans = len(ukv) - 1 if tt == n_current_spans: continue @@ -359,10 +355,8 @@ def _compute_tiling_prerequisites( # First step : insert all required knots into the deformation function # spline if macro_sensitivity: - knot_insertion_matrix = ( - deformation_function_copy.knot_insertion_matrix( - 0, additional_knots[0] - ) + knot_insertion_matrix = deformation_function_copy.knot_insertion_matrix( + 0, additional_knots[0] ) deformation_function_copy.insert_knots(0, additional_knots[0]) for i_pd, akv in enumerate(additional_knots[1:], start=1): @@ -456,19 +450,15 @@ def create( if not self._sanity_check(): raise ValueError("Not enough information provided, abort") - assert isinstance( - knot_span_wise, bool - ), "knot_span_wise must be a bool" - assert isinstance( - macro_sensitivities, bool - ), "macro_senstivities must be a bool" + if isinstance(knot_span_wise, bool) is False: + raise TypeError("knot_span_wise must be a bool") + if isinstance(macro_sensitivities, bool) is False: + raise TypeError("macro_senstivities must be a bool") # check if user wants closed structure if closing_face is not None: if not hasattr(self.microtile, "_closing_tile"): - raise ValueError( - "Microtile does not provide closing tile definition" - ) + raise ValueError("Microtile does not provide closing tile definition") closing_face_dim = {"x": 0, "y": 1, "z": 2}.get(closing_face) if closing_face is None: raise ValueError( @@ -487,9 +477,7 @@ def create( # Check if parametrized is_parametrized = self.parametrization_function is not None - parameter_sensitivities = ( - self.parameter_sensitivity_function is not None - ) + parameter_sensitivities = self.parameter_sensitivity_function is not None if parameter_sensitivities and not is_parametrized: raise ValueError( @@ -534,10 +522,7 @@ def create( # Prepare field for derivatives if macro_sensitivities or parameter_sensitivities: spline_list_derivs = [ - [] - for _ in range( - n_parameter_sensitivities + n_macro_sensitivities - ) + [] for _ in range(n_parameter_sensitivities + n_macro_sensitivities) ] spline_list_ms = [] @@ -559,9 +544,7 @@ def create( # If the sensitivities are requested, evaluate the sensitivity # function, which must be provided by the user if parameter_sensitivities: - tile_sensitivities = self.parameter_sensitivity_function( - positions - ) + tile_sensitivities = self.parameter_sensitivity_function(positions) # To avoid unnecessary constructions (if a parameter # sensitivity evaluates to zero), perform only those that are # in the support of a specific design variable @@ -606,9 +589,7 @@ def create( tile_patch, compute_sensitivities=True ) spline_list_ms.append(composed) - basis_function_compositions.append( - basis_function_composition - ) + basis_function_compositions.append(basis_function_composition) else: for tile_patch in tile: spline_list_ms.append(def_fun.compose(tile_patch)) @@ -644,16 +625,10 @@ def create( control_points = _np.zeros( (cps.shape[0], self._deformation_function.dim) ) - ii_ctps, jj_dim = divmod( - j_cc, self._deformation_function.dim - ) + ii_ctps, jj_dim = divmod(j_cc, self._deformation_function.dim) control_points[:, jj_dim] = mapped_cps[:, ii_ctps] - spline_list_derivs[ - n_parameter_sensitivities + j_cc - ].append( - type(patch_info[0])( - patch_info[0].degrees, control_points - ) + spline_list_derivs[n_parameter_sensitivities + j_cc].append( + type(patch_info[0])(patch_info[0].degrees, control_points) ) # Use a multipatch object to bundle all information @@ -769,9 +744,7 @@ def _sanity_check(self): "attribute `evaluation_points`, that is required for" " a parametrized microstructure construction" ) - result = self._parametrization_function( - self._microtile.evaluation_points - ) + result = self._parametrization_function(self._microtile.evaluation_points) if not isinstance(result, _np.ndarray): raise ValueError( "Function outline of parametrization function must be " @@ -782,9 +755,7 @@ def _sanity_check(self): return True # Check sensitivity function - result = self.parameter_sensitivity_function( - self._microtile.evaluation_points - ) + result = self.parameter_sensitivity_function(self._microtile.evaluation_points) if (not isinstance(result, _np.ndarray)) or (not result.ndim == 3): raise ValueError( "Function outline of parameter sensitivity function must " diff --git a/splinepy/microstructure/tiles/cross_3d_linear.py b/splinepy/microstructure/tiles/cross_3d_linear.py index c41697857..9f23404b4 100644 --- a/splinepy/microstructure/tiles/cross_3d_linear.py +++ b/splinepy/microstructure/tiles/cross_3d_linear.py @@ -63,7 +63,7 @@ def _closing_tile( Six evaluation points with one parameter is used. This parameter describes the radius of the cylinder at the evaluation point. The parameters must be a two-dimensional np.array, where the - value must be between 0.01 and 0.49 + value must be between 0.0 and 0.5 (not inclusive) parameter_sensitivities: np.ndarray(6, 1, para_dim) Describes the parameter sensitivities with respect to some design variable. In case the design variables directly apply to the @@ -201,8 +201,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.maximum( - center_ctps - - _np.array([center_width, v_zero, v_zero]), + center_ctps - _np.array([center_width, v_zero, v_zero]), v_zero, ) if i_derivative == 0 @@ -216,8 +215,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.maximum( - center_ctps - - _np.array([v_zero, center_width, v_zero]), + center_ctps - _np.array([v_zero, center_width, v_zero]), v_zero, ) if i_derivative == 0 @@ -231,8 +229,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.minimum( - center_ctps - + _np.array([center_width, v_zero, v_zero]), + center_ctps + _np.array([center_width, v_zero, v_zero]), v_one, ) if i_derivative == 0 @@ -246,8 +243,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.minimum( - center_ctps - + _np.array([v_zero, center_width, v_zero]), + center_ctps + _np.array([v_zero, center_width, v_zero]), v_one, ) if i_derivative == 0 @@ -355,8 +351,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.maximum( - center_ctps - - _np.array([center_width, v_zero, v_zero]), + center_ctps - _np.array([center_width, v_zero, v_zero]), v_zero, ) if i_derivative == 0 @@ -370,8 +365,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.maximum( - center_ctps - - _np.array([v_zero, center_width, v_zero]), + center_ctps - _np.array([v_zero, center_width, v_zero]), v_zero, ) if i_derivative == 0 @@ -385,8 +379,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.minimum( - center_ctps - + _np.array([center_width, v_zero, v_zero]), + center_ctps + _np.array([center_width, v_zero, v_zero]), v_one, ) if i_derivative == 0 @@ -400,8 +393,7 @@ def _closing_tile( degrees=[1, 1, 1], control_points=( _np.minimum( - center_ctps - + _np.array([v_zero, center_width, v_zero]), + center_ctps + _np.array([v_zero, center_width, v_zero]), v_one, ) if i_derivative == 0 @@ -499,13 +491,12 @@ def create_tile( for i_derivative in range(n_derivatives + 1): # Constant auxiliary values if i_derivative == 0: - [x_min_r, x_max_r, y_min_r, y_max_r, z_min_r, z_max_r] = ( - parameters[:, 0] - ) + [x_min_r, x_max_r, y_min_r, y_max_r, z_min_r, z_max_r] = parameters[ + :, 0 + ] v_one_half = 0.5 center_r = center_expansion * _np.mean(parameters[:, 0]) else: - [x_min_r, x_max_r, y_min_r, y_max_r, z_min_r, z_max_r] = ( parameter_sensitivities[:, :, i_derivative - 1].flatten() ) @@ -530,9 +521,7 @@ def create_tile( ] ) spline_list.append( - _Bezier( - degrees=[1, 1, 1], control_points=center_points + center - ) + _Bezier(degrees=[1, 1, 1], control_points=center_points + center) ) # X-Axis branches diff --git a/splinepy/microstructure/tiles/hollow_octagon.py b/splinepy/microstructure/tiles/hollow_octagon.py index c099e65c0..e50fe53c6 100644 --- a/splinepy/microstructure/tiles/hollow_octagon.py +++ b/splinepy/microstructure/tiles/hollow_octagon.py @@ -57,6 +57,13 @@ def _closing_tile( """ if closure is None: raise ValueError("No closing direction given") + if isinstance(closure, int): + closure = self._closure_directions[closure] + if closure not in self._closure_directions: + raise ValueError( + f"Closure direction {closure} not implemented. " + f"Choose from {self._closure_directions}" + ) parameters, n_derivatives, derivatives = self._process_input( parameters=parameters, diff --git a/splinepy/microstructure/tiles/hollow_octagon_extrude.py b/splinepy/microstructure/tiles/hollow_octagon_extrude.py index 4d63972ac..a49bfc3cb 100644 --- a/splinepy/microstructure/tiles/hollow_octagon_extrude.py +++ b/splinepy/microstructure/tiles/hollow_octagon_extrude.py @@ -244,6 +244,7 @@ def _closing_tile( parameter_sensitivities=None, contact_length=0.2, closure=None, + **kwargs, # noqa ARG002 ): """Create a closing tile to match with closed surface. @@ -257,7 +258,7 @@ def _closing_tile( Describes the parameter sensitivities with respect to some design variable. In case the design variables directly apply to the parameter itself, they evaluate as delta_ij - closure : int + closure : int or str parametric dimension that needs to be closed. Positive values mean that minimum parametric dimension is requested. That means, i.e. -2 closes the tile at maximum z-coordinate. @@ -272,6 +273,13 @@ def _closing_tile( """ if closure is None: raise ValueError("No closing direction given") + if isinstance(closure, int): + closure = self._closure_directions[closure] + if closure not in self._closure_directions: + raise ValueError( + f"Closure direction {closure} not implemented. " + f"Choose from {self._closure_directions}" + ) # Process input parameters, n_derivatives, derivatives = self._process_input( diff --git a/splinepy/microstructure/tiles/tile_base.py b/splinepy/microstructure/tiles/tile_base.py index 2c4975c77..80df767a0 100644 --- a/splinepy/microstructure/tiles/tile_base.py +++ b/splinepy/microstructure/tiles/tile_base.py @@ -52,8 +52,7 @@ def _raise_if_not_set_else_return(cls, attr_name): attr = getattr(cls, attr_name, None) if attr is None and cls is not TileBase: raise NotImplementedError( - f"Inherited Tile-types need to provide {attr_name}, see " - "documentation." + f"Inherited Tile-types need to provide {attr_name}, see documentation." ) return attr @@ -224,9 +223,7 @@ def check_params(self, parameters): parameters.ravel() < upper_bounds ) if not _np.all(within_bounds): - out_of_bounds = parameters[ - ~within_bounds.reshape(parameters.shape) - ] + out_of_bounds = parameters[~within_bounds.reshape(parameters.shape)] raise ValueError( f"The following parameters are out of bounds: {out_of_bounds}. " f"Expected bounds: lower: {lower_bounds} and upper: {upper_bounds}" @@ -249,9 +246,7 @@ def check_param_derivatives(self, derivatives): if derivatives is None: return False - if not ( - isinstance(derivatives, _np.ndarray) and derivatives.ndim == 3 - ): + if not (isinstance(derivatives, _np.ndarray) and derivatives.ndim == 3): raise TypeError("parameters must be three-dimensional np array") if not ( @@ -282,9 +277,7 @@ def create_tile(self, **kwargs): List of list of splines that represents parameter sensitivities. If it is not implemented, returns None. """ - raise NotImplementedError( - f"create_tile() not implemented for {type(self)}" - ) + raise NotImplementedError(f"create_tile() not implemented for {type(self)}") def _process_input(self, parameters, parameter_sensitivities): """Processing input for create_tile and _closing_tile @@ -308,9 +301,7 @@ def _process_input(self, parameters, parameter_sensitivities): # Set parameters to default values if not user-given if parameters is None: default_value = self.default_parameter_value - self._logd( - f"Setting parameters to default values ({default_value})" - ) + self._logd(f"Setting parameters to default values ({default_value})") if isinstance(default_value, float): parameters = _np.full( (len(self.evaluation_points), self.n_info_per_eval_point), @@ -319,6 +310,10 @@ def _process_input(self, parameters, parameter_sensitivities): elif isinstance(default_value, _np.ndarray): parameters = default_value + # Validity check of parameters and their sensitivities + self.check_params(parameters) + self.check_param_derivatives(parameter_sensitivities) + # Initialize list of derivatives if parameter_sensitivities is not None: n_derivatives = parameter_sensitivities.shape[2] @@ -327,10 +322,6 @@ def _process_input(self, parameters, parameter_sensitivities): n_derivatives = 0 derivatives = None - # Validity check of parameters and their sensitivities - self.check_params(parameters) - self.check_param_derivatives(parameter_sensitivities) - return parameters, n_derivatives, derivatives def _check_custom_parameter(self, value, param_name, bounds): @@ -356,6 +347,4 @@ def _check_custom_parameter(self, value, param_name, bounds): raise ValueError(f"Invalid type for {param_name}") if not ((value > min_bound) and (value < max_bound)): - raise ValueError( - f"{param_name} must be in ({min_bound}, {max_bound})" - ) + raise ValueError(f"{param_name} must be in ({min_bound}, {max_bound})") From c45bbe10c8a5995aa45fc9464ab4538aaf72707f Mon Sep 17 00:00:00 2001 From: "clemens.fricke" Date: Tue, 16 Jun 2026 18:18:06 +0200 Subject: [PATCH 171/171] Chore: Update changelog to show release --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cad5d1f5..c182baa29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - v0.2.1 +## [0.2.1] - 2026-06 ### Added @@ -108,7 +108,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`microstructure/tiles`**: Fixed incorrect ordering in finite-difference sensitivity calculation; fixed derivative implementations for `Chi`, `CubeVoid`, `EllipsVoid`, `InverseCross3D`; fixed typo causing errors in `HollowOctagonExtrude` - **`microstructure/microstructure.py`**: Fixed typo in class docstring (`facilitatae` → `facilitate`) - **`multipatch.py`**: Added check to verify that Jacobians exist before computing interface orientations -- **`bspline.py`**: Allow any negative number in the knot vector (previously restricted), fixing issue [#476](https://github.com/tataratat/splinepy/pull/476) +- **`bspline.py`**: Allow any negative number in the knot vector (previously restricted), fixing issue [#476](https://github.com/isosuite/splinepy/pull/476) - **`utils/data.py`**: Minor fixes - **Various IO modules**: Fixed miscellaneous issues and improved error handling @@ -150,7 +150,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial stable release series -[Unreleased]: https://github.com/isosuite/splinepy/compare/v0.2.0...HEAD +[0.2.1]: https://github.com/isosuite/splinepy/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/isosuite/splinepy/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/isosuite/splinepy/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/isosuite/splinepy/compare/v0.1.1...v0.1.2