From 57577b16dee8a62f635c61c2f35cc8f681dc1956 Mon Sep 17 00:00:00 2001 From: Nejc Jurkovic Date: Sun, 29 Dec 2024 22:04:44 +0100 Subject: [PATCH] Introduce a (barely) working InflationGrader Added Distributor classes that calculate best cell distribution and an Approximator class that decides which Chops to choose to meet them; rewrote SmoothGrader base the whole InflationGrader on those. Fixed a bug in grading relations (border case with count=1) To do: - handle cases where both ends of a Wire are on walls; - write tests! - refactor: organize autograding source and its hierarchy and their respective tests - get rid of repeated code (LayerStack in Params and Distributor) - add direct imports - test/use graders with some serious examples --- examples/advanced/inflation_grader.py | 13 +- .../autograding/params/approximator.py | 145 ++++++++++++++++++ .../grading/autograding/params/distributor.py | 99 +++++------- .../grading/autograding/params/inflation.py | 57 ++++--- .../grading/autograding/params/layers.py | 26 +++- src/classy_blocks/grading/relations.py | 2 +- src/classy_blocks/types.py | 4 +- tests/test_grading/test_distributor.py | 2 +- tests/test_grading/test_inflation.py | 119 ++++++++++++++ tests/test_grading/test_params.py | 33 ++-- tests/test_grading/test_relations.py | 2 +- 11 files changed, 385 insertions(+), 117 deletions(-) create mode 100644 src/classy_blocks/grading/autograding/params/approximator.py create mode 100644 tests/test_grading/test_inflation.py diff --git a/examples/advanced/inflation_grader.py b/examples/advanced/inflation_grader.py index 0a6ac81..d5b4cf2 100644 --- a/examples/advanced/inflation_grader.py +++ b/examples/advanced/inflation_grader.py @@ -12,13 +12,20 @@ shape = cb.ExtrudedShape(base, 1) # turn one block around to test grader's skillz -shape.grid[1][0].rotate(np.pi, [0, 0, 1]) +shape.grid[0][1].rotate(np.pi, [0, 0, 1]) mesh.add(shape) mesh.set_default_patch("boundary", "patch") -for i in (0, 1, 2): +for i in (0, 2): shape.operations[i].set_patch("front", "walls") +shape.operations[1].set_patch("back", "walls") + +for i in (3, 4, 5): + shape.operations[i].set_patch("back", "walls") + +for i in (0, 3): + shape.operations[i].set_patch("left", "walls") mesh.modify_patch("walls", "wall") @@ -32,7 +39,7 @@ vertex = list(finder.find_in_sphere(point))[0] vertex.translate([0, 0.8, 0]) -grader = InflationGrader(mesh, 1e-2, 0.1) +grader = InflationGrader(mesh, 5e-3, 0.1) grader.grade(take="max") mesh.write(os.path.join("..", "case", "system", "blockMeshDict"), debug_path="debug.vtk") diff --git a/src/classy_blocks/grading/autograding/params/approximator.py b/src/classy_blocks/grading/autograding/params/approximator.py new file mode 100644 index 0000000..4916969 --- /dev/null +++ b/src/classy_blocks/grading/autograding/params/approximator.py @@ -0,0 +1,145 @@ +import dataclasses +import itertools +from typing import List, Sequence + +import numpy as np + +from classy_blocks.grading.chop import Chop +from classy_blocks.types import FloatListType +from classy_blocks.util.constants import TOL + + +@dataclasses.dataclass +class Piece: + coords: FloatListType + + @property + def point_count(self) -> int: + return len(self.coords) + + @property + def cell_count(self) -> int: + return self.point_count - 1 + + @property + def start_size(self) -> float: + return abs(self.coords[1] - self.coords[0]) + + @property + def end_size(self) -> float: + return abs(self.coords[-1] - self.coords[-2]) + + @property + def length(self) -> float: + return abs(self.coords[-1] - self.coords[0]) + + @property + def total_expansion(self) -> float: + return self.end_size / self.start_size + + @property + def is_simple(self) -> bool: + """If all sizes are equal, this is a simple grading case""" + return abs(self.start_size - self.end_size) < TOL + + def get_chop_coords(self) -> FloatListType: + """Calculates coordinates as will be produced by a Chop""" + if self.is_simple: + return np.linspace(self.coords[0], self.coords[-1], num=self.point_count) + + sizes = np.geomspace(self.start_size, self.end_size, num=self.cell_count) + coords = np.ones(self.point_count) * self.coords[0] + + add = np.cumsum(sizes) + + if self.coords[-1] < self.coords[0]: + add *= -1 + + coords[1:] += add + + return coords + + def get_fitness(self) -> float: + differences = (self.coords - self.get_chop_coords()) ** 2 + return float(np.sum(differences) / self.cell_count) + + def get_chop(self) -> Chop: + return Chop( + length_ratio=self.length, + count=self.cell_count, + total_expansion=self.total_expansion, + ) + + +class Approximator: + """Takes a list of arbitrary cell sizes and creates Chop objects + that blockMesh will understand; Tries to minimize differences + between the rudimentary total expansion + count and actual + desired (optimized) cell sizes. + + In a possible future scenario where a custom mesher would be employed, + actual cell sizes could be used without intervention of this object.""" + + def __init__(self, coords: FloatListType): + self.coords = coords + self.sizes = np.diff(self.coords) + self.ratios = np.roll(self.sizes, -1)[:-1] / self.sizes[:-1] + self.count = len(coords) - 1 + + @property + def length(self) -> float: + return abs(self.coords[-1] - self.coords[0]) + + @property + def is_simple(self) -> bool: + """Returns True if this is a simple one-chop equal-size scenario""" + return max(self.sizes) - min(self.sizes) < TOL + + def get_pieces(self, indexes: List[int]) -> List[Piece]: + """Creates Piece objects between given indexes (adds 0 and -1 automatically); + does not check if count is smaller than length of indexes""" + indexes = [0, *indexes] + + pieces = [Piece(self.coords[indexes[i] : indexes[i + 1] + 1]) for i in range(len(indexes) - 1)] + pieces.append(Piece(self.coords[indexes[-1] :])) + + return pieces + + def get_chops(self, number: int) -> List[Chop]: + if self.is_simple: + # don't bother + return [Chop(count=self.count)] + + # limit number of chops so there's no zero-cell chops + number = min(number + 1, self.count - 1) + + # brute force: choose the best combination of chops + # but don't overdo it; just try a couple of scenarios + refinement = 4 + indexes = np.linspace(0, self.count + 1, num=number * refinement, dtype="int")[1:-1] + + best_fit = 1e12 + best_scenario: Sequence[int] = [0] * (number - 1) + + combinations = itertools.combinations(indexes, r=number - 2) + for scenario in combinations: + try: + pieces = self.get_pieces(scenario) # type:ignore + fit = sum(piece.get_fitness() for piece in pieces) + if fit < best_fit: + best_fit = fit + best_scenario = scenario + except (IndexError, ValueError): + # obviously not the best scenario, eh? + continue + + pieces = self.get_pieces(best_scenario) # type:ignore + + chops = [piece.get_chop() for piece in pieces] + + # normalize length ratios + length = self.length + for chop in chops: + chop.length_ratio = chop.length_ratio / length + + return chops diff --git a/src/classy_blocks/grading/autograding/params/distributor.py b/src/classy_blocks/grading/autograding/params/distributor.py index d948517..a534c5f 100644 --- a/src/classy_blocks/grading/autograding/params/distributor.py +++ b/src/classy_blocks/grading/autograding/params/distributor.py @@ -6,7 +6,7 @@ import scipy.interpolate import scipy.optimize -from classy_blocks.grading.autograding.params.base import sum_length +from classy_blocks.grading.autograding.params.approximator import Approximator from classy_blocks.grading.autograding.params.layers import InflationLayer from classy_blocks.grading.chop import Chop from classy_blocks.types import FloatListType @@ -17,7 +17,7 @@ class DistributorBase(abc.ABC): """Algorithm that creates chops from given count and sizes; first distributes cells using a predefined 'ideal' sizes and ratios, - then optimizes their individual size to get as close to that ideal as possible. + then optimizes their *individual* size to get as close to that ideal as possible. Then, when cells are placed, Chops are created so that they produce as similar cells to the calculated as possible. Since blockMesh only supports equal @@ -32,10 +32,10 @@ class DistributorBase(abc.ABC): @staticmethod def get_actual_ratios(coords: FloatListType) -> FloatListType: + # TODO: repeated code within Approximator! un-repeat lengths = np.diff(coords) - return (lengths[:-1] / np.roll(lengths, -1)[:-1]) ** -1 + return np.roll(lengths, -1)[:-1] / lengths[:-1] - @abc.abstractmethod def get_ideal_ratios(self) -> FloatListType: """Returns desired cell-to-cell ratios""" return np.ones(self.count + 1) @@ -45,7 +45,9 @@ def get_ratio_weights(self) -> FloatListType: """Returns weights of cell ratios""" def get_raw_coords(self) -> FloatListType: - # 'count' denotes number of 'intervals' so there must be another point + # 'count' denotes number of 'intervals' so add one more point to get points; + # first and last cells are added artificially ('ghost cells') to calculate + # proper expansion ratios and sizes return np.concatenate( ([-self.size_before], np.linspace(0, self.length, num=self.count + 1), [self.length + self.size_after]) ) @@ -60,13 +62,15 @@ def get_smooth_coords(self) -> FloatListType: def ratios(inner_coords): coords[2:-2] = inner_coords - difference = self.get_actual_ratios(coords) - self.get_ideal_ratios() - return difference * self.get_ratio_weights() + # to prevent 'flipping' over and producing zero or negative length, scale with e^-ratio + difference = -(self.get_ratio_weights() * (self.get_actual_ratios(coords) - self.get_ideal_ratios())) + return np.exp(difference) - 1 - scale = min(self.size_before, self.size_after, self.length / self.count) / 10 - _ = scipy.optimize.least_squares(ratios, coords[2:-2], method="lm", ftol=scale / 100, x_scale=scale) + scale = min(self.size_before, self.size_after, self.length / self.count) / 100 + _ = scipy.optimize.least_squares(ratios, coords[2:-2], ftol=scale / 10, x_scale=scale) - return coords + # omit the 'ghost' cells + return coords[1:-1] @property def is_simple(self) -> bool: @@ -74,55 +78,15 @@ def is_simple(self) -> bool: base_size = self.length / self.count # TODO: use a more relaxed criterion? - return base_size - self.size_before < TOL and base_size - self.size_after < TOL + return base_size - self.size_before < TOL and base_size - self.size_after < 10 * TOL def get_chops(self, pieces: int) -> List[Chop]: - if self.is_simple: - return [Chop(count=self.count)] + approximator = Approximator(self.get_smooth_coords()) + return approximator.get_chops(pieces) + def get_last_size(self) -> float: coords = self.get_smooth_coords() - sizes = np.diff(coords) - ratios = self.get_actual_ratios(coords) - - count = len(ratios) - 1 - - # create a piecewise linear function from a number of chosen indexes and their respective c2c ratios - # then optimize indexes to obtain best fit - # fratios = scipy.interpolate.interp1d(range(len(ratios)), ratios) - - # def get_piecewise(indexes: List[int]) -> Callable: - # values = np.take(ratios, indexes) - # return scipy.interpolate.interp1d(indexes, values) - - # def get_fitness(indexes: List[int]) -> float: - # fitted = get_piecewise(indexes)(range(len(ratios))) - - # ss_tot = np.sum((ratios - np.mean(ratios)) ** 2) - # ss_res = np.sum((ratios - fitted) ** 2) - - # return 1 - (ss_res / ss_tot) - - # print(get_fitness([0, 9, 10, count])) - # print(get_fitness(np.linspace(0, count, num=pieces + 1, dtype=int))) - - chops: List[Chop] = [] - indexes = np.linspace(0, count, num=pieces + 1, dtype=int) - - for i, index in enumerate(indexes[:-1]): - chop_ratios = ratios[index : indexes[i + 1]] - ratio = np.prod(chop_ratios) - chop_count = indexes[i + 1] - indexes[i] - avg_ratio = ratio ** (1 / chop_count) - length = sum_length(sizes[index], chop_count, avg_ratio) - chop = Chop(length_ratio=length, total_expansion=ratio, count=chop_count) - chops.append(chop) - - # normalize length ratios - sum_ratio = sum([chop.length_ratio for chop in chops]) - for chop in chops: - chop.length_ratio = chop.length_ratio / sum_ratio - - return chops + return coords[-2] - coords[-3] @dataclasses.dataclass @@ -132,8 +96,9 @@ def get_ideal_ratios(self): return super().get_ideal_ratios() def get_ratio_weights(self): - # Enforce stricter policy on size_before and size_after weights = np.ones(self.count + 1) + # Enforce stricter policy on the first few cells + # to match size_before and size_after for i in (0, 1, 2, 3): w = 2 ** (3 - i) weights[i] = w @@ -146,26 +111,34 @@ def get_ratio_weights(self): class InflationDistributor(SmoothDistributor): c2c_expansion: float bl_thickness_factor: int + buffer_expansion: float + bulk_size: float @property def is_simple(self) -> bool: return False def get_ideal_ratios(self): + # TODO: combine this logic and LayerStack; + # possibly package all parameters into a separate dataclass + ratios = super().get_ideal_ratios() + # Ideal growth ratio in boundary layer is user-specified c2c_expansion; inflation_layer = InflationLayer(self.size_before, self.c2c_expansion, self.bl_thickness_factor, 1e12) inflation_count = inflation_layer.count - print(f"Inflation count: {inflation_count}") - - ratios = super().get_ideal_ratios() ratios[:inflation_count] = self.c2c_expansion - print(ratios) + + # add a buffer layer if needed + last_inflation_size = inflation_layer.end_size + if self.bulk_size > self.buffer_expansion * last_inflation_size: + buffer_count = int(np.log(self.bulk_size / last_inflation_size) / np.log(self.buffer_expansion)) + 1 + ratios[inflation_count : inflation_count + buffer_count] = self.buffer_expansion return ratios def get_ratio_weights(self): - return super().get_ratio_weights() - - def _get_ratio_weights(self): + # using the same weights as in SmoothDistributor + # can trigger overflow warnings but doesn't produce + # better chops; thus, keep it simple return np.ones(self.count + 1) diff --git a/src/classy_blocks/grading/autograding/params/inflation.py b/src/classy_blocks/grading/autograding/params/inflation.py index 6a7432b..46c9e9b 100644 --- a/src/classy_blocks/grading/autograding/params/inflation.py +++ b/src/classy_blocks/grading/autograding/params/inflation.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Tuple from classy_blocks.grading.autograding.params.distributor import InflationDistributor, SmoothDistributor from classy_blocks.grading.autograding.params.layers import BufferLayer, BulkLayer, InflationLayer, LayerStack @@ -69,20 +69,7 @@ def get_count(self, length: float, starts_at_wall: bool, ends_at_wall: bool): stack = self.get_stack(length) return stack.count - def is_squeezed(self, count: int, info: WireInfo) -> bool: - if not (info.starts_at_wall or info.ends_at_wall): - return super().is_squeezed(count, info) - - # TODO: replace 0.9 with something less arbitrary (a better rule) - return self.get_stack(info.length).last_size < 0.9 * self.bulk_cell_size - - def get_squeezed_chops(self, count: int, info: WireInfo) -> List[Chop]: - return self.get_chops(count, info) - - def get_chops(self, count, info: WireInfo) -> List[Chop]: - if info.starts_at_wall and info.ends_at_wall: - raise NotImplementedError - + def get_sizes(self, info: WireInfo) -> Tuple[float, float]: size_before = info.size_before if size_before is None: if info.starts_at_wall: @@ -97,18 +84,46 @@ def get_chops(self, count, info: WireInfo) -> List[Chop]: else: size_after = self.cell_size + return size_before, size_after + + def is_squeezed(self, count: int, info: WireInfo) -> bool: + if not (info.starts_at_wall or info.ends_at_wall): + return super().is_squeezed(count, info) + + if info.starts_at_wall and info.ends_at_wall: + raise NotImplementedError + + stack = self.get_stack(info.length, info.size_after) + + if len(stack.layers) < 3: + return True + + if stack.count < count: + return True + + return False + + def get_chops(self, count, info: WireInfo) -> List[Chop]: + # TODO: un-if-if-if + if info.starts_at_wall and info.ends_at_wall: + raise NotImplementedError + + size_before, size_after = self.get_sizes(info) + if not (info.starts_at_wall or info.ends_at_wall): - print("NOT AT WALL", info) distributor = SmoothDistributor(count, size_before, info.length, size_after) else: - print("AT WALL", info) distributor = InflationDistributor( - count, size_before, info.length, size_after, self.c2c_expansion, self.bl_thickness_factor + count, + size_before, + info.length, + size_after, + self.c2c_expansion, + self.bl_thickness_factor, + self.buffer_expansion, + self.bulk_cell_size, ) chops = distributor.get_chops(3) - if info.ends_at_wall: - return list(reversed(chops)) - return chops diff --git a/src/classy_blocks/grading/autograding/params/layers.py b/src/classy_blocks/grading/autograding/params/layers.py index caa2119..11a831c 100644 --- a/src/classy_blocks/grading/autograding/params/layers.py +++ b/src/classy_blocks/grading/autograding/params/layers.py @@ -23,7 +23,7 @@ def _construct( size = self.start_size length = self.start_size - for i in range(count_limit): + for _ in range(count_limit): if length >= length_limit: break @@ -35,7 +35,7 @@ def _construct( length += size size *= self.c2c_expansion - count = i + count += 1 return length, size, count @@ -45,11 +45,11 @@ def __init__(self, length_limit: float = VBIG, count_limit: int = 10**12, size_l class InflationLayer(Layer): - def __init__(self, wall_size: float, c2c_expansion: float, thickness_factor: int, _max_length: float): + def __init__(self, wall_size: float, c2c_expansion: float, thickness_factor: int, max_length: float): self.start_size = wall_size self.c2c_expansion = c2c_expansion - super().__init__(length_limit=thickness_factor * wall_size) + super().__init__(length_limit=min(thickness_factor * wall_size, max_length)) class BufferLayer(Layer): @@ -67,9 +67,18 @@ def __init__(self, start_size: float, end_size: float, remainder: float): self.end_size = end_size self.length = remainder - total_expansion = gr.get_total_expansion__start_size__end_size(self.length, self.start_size, self.end_size) - self.count = gr.get_count__total_expansion__start_size(self.length, total_expansion, self.start_size) - self.c2c_expansion = gr.get_c2c_expansion__count__end_size(self.length, self.count, self.end_size) + # TODO: handle this in a more dignified way + if remainder < min(start_size, end_size): + self.count = 0 + self.c2c_expansion = 1 + else: + total_expansion = gr.get_total_expansion__start_size__end_size(self.length, self.start_size, self.end_size) + self.count = gr.get_count__total_expansion__start_size(self.length, total_expansion, self.start_size) + + if self.count < 2: + self.c2c_expansion = 1 + else: + self.c2c_expansion = gr.get_c2c_expansion__count__end_size(self.length, self.count, self.end_size) class LayerStack: @@ -88,7 +97,8 @@ def remaining_length(self) -> float: return self.length - sum(layer.length for layer in self.layers) def add(self, layer: Layer) -> bool: - self.layers.append(layer) + if layer.count > 0: + self.layers.append(layer) return self.is_done @property diff --git a/src/classy_blocks/grading/relations.py b/src/classy_blocks/grading/relations.py index f3f6eac..6db637c 100644 --- a/src/classy_blocks/grading/relations.py +++ b/src/classy_blocks/grading/relations.py @@ -195,7 +195,7 @@ def fexp(c2c): def get_c2c_expansion__count__end_size(length, count, end_size): """Calculates cell-to-cell expansion ratio from given count and end size""" _validate_length(length) - _validate_count(count, ">=1") + _validate_count(count, ">=2") _validate_start_end_size(end_size, "end") if abs(count * end_size - length) / length < constants.TOL: diff --git a/src/classy_blocks/types.py b/src/classy_blocks/types.py index 903cfff..20f4eee 100644 --- a/src/classy_blocks/types.py +++ b/src/classy_blocks/types.py @@ -1,9 +1,9 @@ from typing import Any, Callable, List, Literal, Sequence, Tuple, TypedDict, Union -from nptyping import NDArray, Shape +from nptyping import Float, NDArray, Shape # A plain list of floats -FloatListType = NDArray[Shape["1, *"], Any] +FloatListType = NDArray[Shape["1, *"], Float] # A single point can be specified as a list of floats or as a numpy array NPPointType = NDArray[Shape["3, 1"], Any] diff --git a/tests/test_grading/test_distributor.py b/tests/test_grading/test_distributor.py index 5454e10..96f54ad 100644 --- a/tests/test_grading/test_distributor.py +++ b/tests/test_grading/test_distributor.py @@ -18,7 +18,7 @@ def test_get_smooth_simple(self): # just to make sure it runs smoother = SmoothDistributor(10, 0.1, 1, 0.1) - np.testing.assert_almost_equal(smoother.get_smooth_coords(), np.arange(-0.1, 1.1, 0.1), decimal=5) + np.testing.assert_almost_equal(smoother.get_smooth_coords(), np.arange(0, 1.1, 0.1), decimal=5) def test_raw_variable(self): smoother = SmoothDistributor(10, 0.1, 1, 0.01) diff --git a/tests/test_grading/test_inflation.py b/tests/test_grading/test_inflation.py new file mode 100644 index 0000000..1e48fbb --- /dev/null +++ b/tests/test_grading/test_inflation.py @@ -0,0 +1,119 @@ +import unittest + +import numpy as np +from parameterized import parameterized + +from classy_blocks.grading.autograding.params.approximator import Approximator, Piece + + +def get_coords(count, c2c_expansion, start_coord, start_size): + expansions = np.cumprod(np.ones(count) * c2c_expansion) + sizes = np.ones(count) * start_size * expansions + coords = np.ones(count + 1) * start_coord + coords[1:] += np.cumsum(sizes) + + return coords + + +class PieceTests(unittest.TestCase): + def test_piece_coords_linear(self): + given_coords = np.linspace(0.1, 1.1, num=10) + piece = Piece(given_coords) + + np.testing.assert_almost_equal(given_coords, piece.get_chop_coords()) + + @parameterized.expand( + ( + (0.1, 1, 10), + (0.1, 10, 10), + (-10, -1, 10), + (-1, -10, 10), + ) + ) + def test_piece_coords_geom(self, start, end, count): + given_coords = np.geomspace(start, end, num=count) + piece = Piece(given_coords) + + np.testing.assert_almost_equal(given_coords, piece.get_chop_coords()) + + def test_piece_coords_mixed_sign(self): + given_coords = get_coords(10, 1.2, -0.1, 0.1) + + piece = Piece(given_coords) + + np.testing.assert_almost_equal(given_coords, piece.get_chop_coords()) + + def test_piece_coords_flipped(self): + given_coords = np.flip(get_coords(10, 1.2, -0.1, 0.1)) + + piece = Piece(given_coords) + + np.testing.assert_almost_equal(given_coords, piece.get_chop_coords()) + + def test_fitness_perfect(self): + given_coords = get_coords(10, 1.2, 0, 0.1) + piece = Piece(given_coords) + + self.assertAlmostEqual(piece.get_fitness(), 0) + + def test_fitness_bad(self): + given_coords = get_coords(10, 1.2, 0, 0.1) + given_coords[1] -= 0.05 + given_coords[2] += 0.05 + given_coords[3] += 0.1 + piece = Piece(given_coords) + + self.assertGreater(piece.get_fitness(), 0) + + def test_fitness_worse(self): + chop_coords = get_coords(10, 1.2, 0, 0.1) + + actual_coords_1 = np.copy(chop_coords) + actual_coords_1[2] += 0.05 + piece_1 = Piece(actual_coords_1) + + actual_coords_2 = np.copy(chop_coords) + actual_coords_2[2] += 0.05 + actual_coords_2[3] += 0.05 + piece_2 = Piece(actual_coords_2) + + self.assertGreater(piece_2.get_fitness(), piece_1.get_fitness()) + + +class ApproximatorTests(unittest.TestCase): + def test_is_simple(self): + coords = np.linspace(-0.1, 0.1, num=20) + + self.assertTrue(Approximator(coords).is_simple) + + def test_is_not_simple(self): + coords = np.linspace(-1, 1, num=10) + coords[1] += 0.05 + + self.assertFalse(Approximator(coords).is_simple) + + @parameterized.expand( + ( + ([2, 7],), + ([2, 4, 6, 8],), + ([3, 5, 8],), + ) + ) + def test_get_pieces(self, indexes): + coords = get_coords(10, 1.2, 0.1, 0.1) + appr = Approximator(coords) + + pieces = appr.get_pieces(indexes) + self.assertEqual(len(pieces), len(indexes) + 1) + + def test_get_pieces_continuity(self): + coords = get_coords(19, 1.2, 0.1, 0.1) + appr = Approximator(coords) + pieces = appr.get_pieces([5, 10, 15]) + + piece_coords = [] + for piece in pieces: + piece_coords += piece.coords.tolist()[:-1] + piece_coords.append(pieces[-1].coords[-1]) + + np.testing.assert_array_equal(coords, piece_coords) diff --git a/tests/test_grading/test_params.py b/tests/test_grading/test_params.py index c560890..075f066 100644 --- a/tests/test_grading/test_params.py +++ b/tests/test_grading/test_params.py @@ -23,14 +23,14 @@ def test_get_count_bulk(self): @parameterized.expand( ( - (1, 24), - (2, 34), - (3, 44), - (0.1, 16), - (0.05, 14), + (1, 25), + (2, 35), + (3, 45), + (0.1, 17), + (0.05, 17), (0.01, 7), (0.002, 2), - (0.001, 2), + (0.001, 1), (0.0005, 1), ) ) @@ -41,13 +41,13 @@ def test_get_count_wall(self, length, count): @parameterized.expand( ( - (1, 38), # 0 - (2, 48), # 1 - (3, 58), # 2 - (0.1, 28), # 3 - (0.05, 20), # 4 - (0.01, 8), # 5 - (0.002, 4), # 6 + (1, 40), # 0 + (2, 50), # 1 + (3, 60), # 2 + (0.1, 34), # 3 + (0.05, 22), # 4 + (0.01, 10), # 5 + (0.002, 2), # 6 (0.001, 2), # 7 (0.0005, 2), # 8 ) @@ -67,12 +67,11 @@ def test_get_count_double_wall(self, length, count): (3, 44, False), # 2 (0.3, 16, False), # 3 # squeezed: enough room, high cell count - (1, 25, True), # 4 - (2, 35, True), # 5 - (3, 45, True), # 6 + (0.1, 25, True), # 4 + (1, 35, True), # 5 + (2, 45, True), # 6 # squeezed: not enough room, cell count doesn't matter (0.2, 16, True), # 7 - (0.2, 0, True), # 8 ) ) def test_is_squeezed_wall(self, length, count, squeezed): diff --git a/tests/test_grading/test_relations.py b/tests/test_grading/test_relations.py index c560f92..909f7db 100644 --- a/tests/test_grading/test_relations.py +++ b/tests/test_grading/test_relations.py @@ -160,7 +160,7 @@ def test_get_c2c_expansion__count__start_size_invalid(self, args): ((1, 10, 0.1), 1), ((1, 10, 0.01), 0.6784573173), ((1, 10, 0.2), 1.202420088), - ((1, 1, 1), 1), # border case + ((1, 2, 0.5), 1), # border case ) ) def test_get_c2c_expansion__count__end_size_valid(self, args, result):