mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
alignment api: read spiral transition curves and cant from PI method CSV
Thin CSV front end for the spiral and cant support in the PI method core. The import can now start with an optional header row naming the values that define each horizontal PI. The number of header columns selects the format: X,Y,R (the existing format, also used when there is no header), X,Y,R,Lin,Lout for spiral transition curves, and X,Y,R,Lin,Lout,E for spiral transition curves with cant. The parser only groups columns and forwards them to create() and layout_horizontal_alignment_by_pi_method(); all geometry lives in the core, so this surface stays small and remains removable without affecting API users, per the maintainer guidance that the CSV import is a stop-gap. Files in the existing format import unchanged; the output is byte-identical apart from GUIDs. Pure column-count auto-detection would be ambiguous (15 values parse as 5 legacy PIs or 3 spiral PIs), so an explicit header is required for the new groups, and no existing file can collide with one because row 1 of a headerless file must parse as numbers. Since cant requires horizontal, vertical, and cant layouts, at least one vertical row is required when the cant format is used. create_from_csv() accepts an optional rail_head_distance that is assigned to IfcAlignmentCant.RailHeadDistance. Fixes #6890 Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -17,23 +17,80 @@
|
|||||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api.alignment
|
import ifcopenshell.api.alignment
|
||||||
from ifcopenshell import entity_instance
|
from ifcopenshell import entity_instance
|
||||||
|
|
||||||
|
# number of values per horizontal PI for each supported format
|
||||||
|
_horizontal_group_sizes = (3, 5, 6)
|
||||||
|
|
||||||
def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
|
|
||||||
|
def _is_number(value: str) -> bool:
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_horizontal_row(data: list[float], group_size: int):
|
||||||
|
"""
|
||||||
|
Parses the horizontal row into PI coordinates, radii (optionally with spiral transition
|
||||||
|
lengths), and cants. The curve values of the first and last PI are placeholders and are
|
||||||
|
discarded.
|
||||||
|
"""
|
||||||
|
if len(data) % group_size != 0 or len(data) // group_size < 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"expected the horizontal row to have {group_size} values per PI for at least two PIs, "
|
||||||
|
f"instead received {len(data)} values"
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = [data[k : k + group_size] for k in range(0, len(data), group_size)]
|
||||||
|
coordinates = [(g[0], g[1]) for g in groups]
|
||||||
|
|
||||||
|
cants: Optional[list[float]] = None
|
||||||
|
if group_size == 3:
|
||||||
|
radii = [g[2] for g in groups[1:-1]]
|
||||||
|
elif group_size == 5:
|
||||||
|
radii = [(g[2], g[3], g[4]) for g in groups[1:-1]]
|
||||||
|
else:
|
||||||
|
radii = [(g[2], g[3], g[4]) for g in groups[1:-1]]
|
||||||
|
cants = [g[5] for g in groups[1:-1]]
|
||||||
|
|
||||||
|
return coordinates, radii, cants
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_vertical_row(data: list[float]):
|
||||||
|
"""
|
||||||
|
Parses a vertical row into VPI coordinates and vertical curve lengths. The length values of the
|
||||||
|
first and last VPI are placeholders and are discarded.
|
||||||
|
"""
|
||||||
|
if len(data) % 3 != 0 or len(data) // 3 < 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"expected a vertical row to have 3 values per VPI for at least two VPIs, "
|
||||||
|
f"instead received {len(data)} values"
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = [data[k : k + 3] for k in range(0, len(data), 3)]
|
||||||
|
coordinates = [(g[0], g[1]) for g in groups]
|
||||||
|
lengths = [g[2] for g in groups[1:-1]]
|
||||||
|
|
||||||
|
return coordinates, lengths
|
||||||
|
|
||||||
|
|
||||||
|
def create_from_csv(file: ifcopenshell.file, filepath: str, rail_head_distance: float = 1.0) -> entity_instance:
|
||||||
"""
|
"""
|
||||||
Creates an alignment from PI data stored in a CSV file.
|
Creates an alignment from PI data stored in a CSV file.
|
||||||
|
|
||||||
The format of the file is:
|
The format of the file is:
|
||||||
|
|
||||||
X1,Y1,R1,X2,Y2,R2 ... Xn-1,Yn-1,Rn-1,Xn,Yn
|
X1,Y1,R1,X2,Y2,R2 ... Xn,Yn,Rn
|
||||||
|
|
||||||
D1,Z1,L1,D2,Z2,L2 ... Dn-1,Zn-1,Ln-1,Dn,Zn
|
D1,Z1,L1,D2,Z2,L2 ... Dn,Zn,Ln
|
||||||
|
|
||||||
D1,Z1,L1,D2,Z2,L2 ... Dn-1,Zn-1,Ln-1,Dn,Zn
|
D1,Z1,L1,D2,Z2,L2 ... Dn,Zn,Ln
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -48,56 +105,96 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
|
|||||||
|
|
||||||
R1 and Rn, as well as L1 and Ln are placeholders and not used. They are recommended to have values of 0.0.
|
R1 and Rn, as well as L1 and Ln are placeholders and not used. They are recommended to have values of 0.0.
|
||||||
|
|
||||||
R2 and Rn-2 are the radii of the first and last horizontal curves.
|
|
||||||
|
|
||||||
L2 and Ln-2 are the length of the first and last vertical curves.
|
|
||||||
|
|
||||||
The CSV file contains one horizontal alignment, zero, one, or more vertical alignments
|
The CSV file contains one horizontal alignment, zero, one, or more vertical alignments
|
||||||
|
|
||||||
|
Optionally, the file can begin with a header row naming the values that define each horizontal
|
||||||
|
PI. The number of header columns sets the format of the horizontal row:
|
||||||
|
|
||||||
|
X,Y,R - PI coordinates and circular curve radius (the default format described above)
|
||||||
|
|
||||||
|
X,Y,R,Lin,Lout - adds clothoid spiral transition curves of length Lin ahead of the circular
|
||||||
|
curve and Lout following the circular curve. Use 0.0 for a spiral-less connection.
|
||||||
|
|
||||||
|
X,Y,R,Lin,Lout,E - adds a cant profile. E is the cant of the curve, in the project length
|
||||||
|
unit, applied to the rail on the outside of the curve. The cant varies linearly over the
|
||||||
|
spiral transition curves, is constant over the circular curve, and is zero on tangent runs.
|
||||||
|
Curves with a non-zero cant require non-zero Lin and Lout so the cant profile is continuous.
|
||||||
|
|
||||||
|
As with R, the Lin, Lout, and E values of the first and last PI are placeholders and are
|
||||||
|
recommended to have values of 0.0. Vertical rows always have 3 values per VPI. A cant layout is
|
||||||
|
created only for the X,Y,R,Lin,Lout,E format, and at least one vertical alignment row is
|
||||||
|
required in that case.
|
||||||
|
|
||||||
:param filepath: path the to CSV file
|
:param filepath: path the to CSV file
|
||||||
|
:param rail_head_distance: value assigned to IfcAlignmentCant.RailHeadDistance when a cant layout is created
|
||||||
:return: IfcAlignment
|
:return: IfcAlignment
|
||||||
"""
|
"""
|
||||||
alignment = None
|
alignment = None
|
||||||
|
group_size = 3
|
||||||
|
include_cant = False
|
||||||
|
vertical_count = 0
|
||||||
with open(filepath, newline="") as csvfile:
|
with open(filepath, newline="") as csvfile:
|
||||||
reader = csv.reader(csvfile)
|
reader = csv.reader(csvfile)
|
||||||
row_count = 0
|
row_count = 0
|
||||||
for row in reader:
|
for row in reader:
|
||||||
|
if row_count == 0 and len(row) and not _is_number(row[0]):
|
||||||
|
# the first row is a header row naming the horizontal PI values
|
||||||
|
header = [column for column in row if column.strip()]
|
||||||
|
if len(header) not in _horizontal_group_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"expected the header row to have {_horizontal_group_sizes} columns, "
|
||||||
|
f"instead received {len(header)} columns"
|
||||||
|
)
|
||||||
|
group_size = len(header)
|
||||||
|
continue
|
||||||
|
|
||||||
data = list(map(float, row)) # Convert all values to float
|
data = list(map(float, row)) # Convert all values to float
|
||||||
coordinates: list[list[float]] = (
|
|
||||||
[]
|
|
||||||
) # horizontal coordinates for first row, vertical coordinates for subsequent rows
|
|
||||||
radii: list[float] = [] # horizontal curve radii for first row, vertical curve length for subsequent rows
|
|
||||||
|
|
||||||
row_count += 1
|
row_count += 1
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(data):
|
|
||||||
if i + 1 < len(data):
|
|
||||||
x, y = float(data[i]), float(data[i + 1])
|
|
||||||
coordinates.append((x, y)) # Store (X, Y) pair
|
|
||||||
i += 2
|
|
||||||
if i < len(data) and (i + 1) % 3 == 0: # Every third element after an (X,Y) pair is R
|
|
||||||
radii.append(data[i])
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
radii = radii[1:-1] # The first radius value is a placeholder, remove it
|
|
||||||
|
|
||||||
if row_count == 1:
|
if row_count == 1:
|
||||||
alignment = ifcopenshell.api.alignment.create(file, "Alignment_from_CSV")
|
coordinates, radii, cants = _parse_horizontal_row(data, group_size)
|
||||||
|
include_cant = cants is not None
|
||||||
|
if include_cant:
|
||||||
|
# cant requires horizontal, vertical, and cant layouts. the vertical layout is
|
||||||
|
# populated by the first vertical row.
|
||||||
|
alignment = ifcopenshell.api.alignment.create(
|
||||||
|
file,
|
||||||
|
"Alignment_from_CSV",
|
||||||
|
include_vertical=True,
|
||||||
|
include_cant=True,
|
||||||
|
rail_head_distance=rail_head_distance,
|
||||||
|
)
|
||||||
|
cant_layout = ifcopenshell.api.alignment.get_cant_layout(alignment)
|
||||||
|
else:
|
||||||
|
alignment = ifcopenshell.api.alignment.create(file, "Alignment_from_CSV")
|
||||||
|
cant_layout = None
|
||||||
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(
|
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(
|
||||||
file, horizontal_layout, coordinates, radii
|
file, horizontal_layout, coordinates, radii, cant_layout=cant_layout, cants=cants
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# add all subsequent vertical alignments
|
# add all subsequent vertical alignments
|
||||||
assert alignment is not None
|
assert alignment is not None
|
||||||
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
|
coordinates, lengths = _parse_vertical_row(data)
|
||||||
|
vertical_count += 1
|
||||||
|
if include_cant and vertical_count == 1:
|
||||||
|
# the vertical layout was created along with the cant layout
|
||||||
|
vertical_layout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
|
||||||
|
else:
|
||||||
|
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
|
||||||
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(
|
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(
|
||||||
file, vertical_layout, coordinates, radii
|
file, vertical_layout, coordinates, lengths
|
||||||
)
|
)
|
||||||
|
|
||||||
if row_count == 0:
|
if row_count == 0:
|
||||||
raise ValueError(f"CSV file '{filepath}' is empty; expected at least one row for the horizontal alignment.")
|
raise ValueError(f"CSV file '{filepath}' is empty; expected at least one row for the horizontal alignment.")
|
||||||
|
|
||||||
|
if include_cant and vertical_count == 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"CSV file '{filepath}' has a cant profile but no vertical alignment; "
|
||||||
|
"at least one vertical alignment row is required with cant."
|
||||||
|
)
|
||||||
|
|
||||||
assert alignment is not None
|
assert alignment is not None
|
||||||
return alignment
|
return alignment
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# IfcOpenShell - IFC toolkit and geometry engine
|
||||||
|
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcOpenShell.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This file was generated with the assistance of an AI coding tool.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import ifcopenshell.api.alignment
|
||||||
|
import ifcopenshell.api.context
|
||||||
|
import ifcopenshell.api.unit
|
||||||
|
|
||||||
|
|
||||||
|
def _create_file():
|
||||||
|
file = ifcopenshell.file(schema="IFC4X3")
|
||||||
|
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test")
|
||||||
|
length = ifcopenshell.api.unit.add_si_unit(file, unit_type="LENGTHUNIT")
|
||||||
|
ifcopenshell.api.unit.assign_unit(file, units=[length])
|
||||||
|
geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model")
|
||||||
|
axis_model_representation_subcontext = ifcopenshell.api.context.add_context(
|
||||||
|
file,
|
||||||
|
context_type="Model",
|
||||||
|
context_identifier="Axis",
|
||||||
|
target_view="MODEL_VIEW",
|
||||||
|
parent=geometric_representation_context,
|
||||||
|
)
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
|
def _write_csv(content: str) -> str:
|
||||||
|
handle, filepath = tempfile.mkstemp(suffix=".csv", text=True)
|
||||||
|
with os.fdopen(handle, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
|
def _get_horizontal_segments(alignment):
|
||||||
|
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||||
|
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal_layout)
|
||||||
|
return segment_nest.RelatedObjects
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_from_csv():
|
||||||
|
file = _create_file()
|
||||||
|
|
||||||
|
filepath = _write_csv(
|
||||||
|
"500,2500,0,3340,660,1000,4340,5000,1250,7600,4560,950,8480,2010,0\n"
|
||||||
|
"0,100,0,2000,135,1600,5000,105,1200,9800,105,0\n"
|
||||||
|
)
|
||||||
|
alignment = ifcopenshell.api.alignment.create_from_csv(file, filepath)
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
assert alignment.Name == "Alignment_from_CSV"
|
||||||
|
|
||||||
|
segments = _get_horizontal_segments(alignment)
|
||||||
|
assert len(segments) == 8 # 4 tangent runs + 3 circular curves + zero length segment
|
||||||
|
expected_types = ["LINE", "CIRCULARARC", "LINE", "CIRCULARARC", "LINE", "CIRCULARARC", "LINE", "LINE"]
|
||||||
|
assert [s.DesignParameters.PredefinedType for s in segments] == expected_types
|
||||||
|
|
||||||
|
# the CSV result matches the result of create_by_pi_method with the same PI data
|
||||||
|
file2 = _create_file()
|
||||||
|
alignment2 = ifcopenshell.api.alignment.create_by_pi_method(
|
||||||
|
file2,
|
||||||
|
"TestAlignment",
|
||||||
|
[(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)],
|
||||||
|
[(1000.0), (1250.0), (950.0)],
|
||||||
|
[(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (9800.0, 105.0)],
|
||||||
|
[(1600.0), (1200.0)],
|
||||||
|
)
|
||||||
|
segments2 = _get_horizontal_segments(alignment2)
|
||||||
|
assert len(segments) == len(segments2)
|
||||||
|
for s1, s2 in zip(segments, segments2):
|
||||||
|
d1 = s1.DesignParameters
|
||||||
|
d2 = s2.DesignParameters
|
||||||
|
assert d1.PredefinedType == d2.PredefinedType
|
||||||
|
assert d1.StartPoint.Coordinates == pytest.approx(d2.StartPoint.Coordinates)
|
||||||
|
assert d1.StartDirection == pytest.approx(d2.StartDirection)
|
||||||
|
assert d1.StartRadiusOfCurvature == pytest.approx(d2.StartRadiusOfCurvature)
|
||||||
|
assert d1.EndRadiusOfCurvature == pytest.approx(d2.EndRadiusOfCurvature)
|
||||||
|
assert d1.SegmentLength == pytest.approx(d2.SegmentLength)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_from_csv_spiral_transitions():
|
||||||
|
file = _create_file()
|
||||||
|
|
||||||
|
filepath = _write_csv(
|
||||||
|
"X,Y,R,Lin,Lout\n"
|
||||||
|
"500,2500,0,0,0,3340,660,1000,200,150,4340,5000,1250,180,180,7600,4560,950,0,120,8480,2010,0,0,0\n"
|
||||||
|
"0,100,0,2000,135,1600,5000,105,1200,9800,105,0\n"
|
||||||
|
)
|
||||||
|
alignment = ifcopenshell.api.alignment.create_from_csv(file, filepath)
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
segments = _get_horizontal_segments(alignment)
|
||||||
|
expected_types = [
|
||||||
|
"LINE",
|
||||||
|
"CLOTHOID",
|
||||||
|
"CIRCULARARC",
|
||||||
|
"CLOTHOID",
|
||||||
|
"LINE",
|
||||||
|
"CLOTHOID",
|
||||||
|
"CIRCULARARC",
|
||||||
|
"CLOTHOID",
|
||||||
|
"LINE",
|
||||||
|
"CIRCULARARC",
|
||||||
|
"CLOTHOID",
|
||||||
|
"LINE",
|
||||||
|
"LINE", # zero length segment
|
||||||
|
]
|
||||||
|
assert [s.DesignParameters.PredefinedType for s in segments] == expected_types
|
||||||
|
|
||||||
|
# spirals run from zero curvature to the curve radius and vice versa
|
||||||
|
entry_spiral = segments[1].DesignParameters
|
||||||
|
assert entry_spiral.StartRadiusOfCurvature == 0.0
|
||||||
|
assert entry_spiral.EndRadiusOfCurvature == pytest.approx(1000.0) # positive, curve to the left
|
||||||
|
assert entry_spiral.SegmentLength == pytest.approx(200.0)
|
||||||
|
exit_spiral = segments[3].DesignParameters
|
||||||
|
assert exit_spiral.StartRadiusOfCurvature == pytest.approx(1000.0)
|
||||||
|
assert exit_spiral.EndRadiusOfCurvature == 0.0
|
||||||
|
assert exit_spiral.SegmentLength == pytest.approx(150.0)
|
||||||
|
assert segments[5].DesignParameters.EndRadiusOfCurvature == pytest.approx(-1250.0) # curve to the right
|
||||||
|
|
||||||
|
# the CSV result matches the pure PI method solution for the same PI data
|
||||||
|
# (geometric continuity of the solution is covered by test_solve_horizontal_alignment_by_pi_method)
|
||||||
|
solved = ifcopenshell.api.alignment.solve_horizontal_alignment_by_pi_method(
|
||||||
|
[(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)],
|
||||||
|
[(1000.0, 200.0, 150.0), (1250.0, 180.0, 180.0), (950.0, 0.0, 120.0)],
|
||||||
|
)
|
||||||
|
assert len(solved) == len(segments) - 1 # the file has an additional zero length segment
|
||||||
|
for definition, segment in zip(solved, segments):
|
||||||
|
d = segment.DesignParameters
|
||||||
|
assert d.PredefinedType == definition.predefined_type
|
||||||
|
assert d.StartPoint.Coordinates == pytest.approx(definition.start_point)
|
||||||
|
assert d.StartDirection == pytest.approx(definition.start_direction)
|
||||||
|
assert d.StartRadiusOfCurvature == pytest.approx(definition.start_radius_of_curvature)
|
||||||
|
assert d.EndRadiusOfCurvature == pytest.approx(definition.end_radius_of_curvature)
|
||||||
|
assert d.SegmentLength == pytest.approx(definition.segment_length)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_from_csv_cant():
|
||||||
|
file = _create_file()
|
||||||
|
|
||||||
|
filepath = _write_csv(
|
||||||
|
"X,Y,R,Lin,Lout,E\n"
|
||||||
|
"500,2500,0,0,0,0,3340,660,1000,200,150,0.15,4340,5000,1250,180,180,0.12,7600,4560,950,140,120,0.1,8480,2010,0,0,0,0\n"
|
||||||
|
"0,100,0,2000,135,1600,5000,105,1200,9800,105,0\n"
|
||||||
|
)
|
||||||
|
alignment = ifcopenshell.api.alignment.create_from_csv(file, filepath, rail_head_distance=1.5)
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
cant_layout = ifcopenshell.api.alignment.get_cant_layout(alignment)
|
||||||
|
assert cant_layout is not None
|
||||||
|
assert cant_layout.RailHeadDistance == pytest.approx(1.5)
|
||||||
|
|
||||||
|
vertical_layout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
|
||||||
|
assert vertical_layout is not None
|
||||||
|
|
||||||
|
horizontal_segments = _get_horizontal_segments(alignment)
|
||||||
|
cant_segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(cant_layout)
|
||||||
|
cant_segments = cant_segment_nest.RelatedObjects
|
||||||
|
|
||||||
|
# cant segments correspond one-for-one with the horizontal segments
|
||||||
|
assert len(cant_segments) == len(horizontal_segments)
|
||||||
|
|
||||||
|
dist_along = 0.0
|
||||||
|
for horizontal_segment, cant_segment in zip(horizontal_segments[:-1], cant_segments[:-1]):
|
||||||
|
h = horizontal_segment.DesignParameters
|
||||||
|
c = cant_segment.DesignParameters
|
||||||
|
assert c.StartDistAlong == pytest.approx(dist_along)
|
||||||
|
assert c.HorizontalLength == pytest.approx(h.SegmentLength)
|
||||||
|
if h.PredefinedType == "CLOTHOID":
|
||||||
|
assert c.PredefinedType == "LINEARTRANSITION"
|
||||||
|
else:
|
||||||
|
assert c.PredefinedType == "CONSTANTCANT"
|
||||||
|
dist_along += h.SegmentLength
|
||||||
|
|
||||||
|
# the first curve is to the left, so the cant is applied to the right rail
|
||||||
|
entry_cant = cant_segments[1].DesignParameters
|
||||||
|
assert entry_cant.StartCantLeft == pytest.approx(0.0)
|
||||||
|
assert entry_cant.EndCantLeft == pytest.approx(0.0)
|
||||||
|
assert entry_cant.StartCantRight == pytest.approx(0.0)
|
||||||
|
assert entry_cant.EndCantRight == pytest.approx(0.15)
|
||||||
|
curve_cant = cant_segments[2].DesignParameters
|
||||||
|
assert curve_cant.StartCantRight == pytest.approx(0.15)
|
||||||
|
|
||||||
|
# the second curve is to the right, so the cant is applied to the left rail
|
||||||
|
assert cant_segments[6].DesignParameters.StartCantLeft == pytest.approx(0.12)
|
||||||
|
assert cant_segments[6].DesignParameters.StartCantRight == pytest.approx(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_from_csv_cant_requires_transitions():
|
||||||
|
file = _create_file()
|
||||||
|
|
||||||
|
filepath = _write_csv(
|
||||||
|
"X,Y,R,Lin,Lout,E\n"
|
||||||
|
"500,2500,0,0,0,0,3340,660,1000,0,0,0.15,4340,5000,0,0,0,0\n"
|
||||||
|
"0,100,0,2000,135,1600,5000,105,0\n"
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ifcopenshell.api.alignment.create_from_csv(file, filepath)
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_from_csv_cant_requires_vertical():
|
||||||
|
file = _create_file()
|
||||||
|
|
||||||
|
filepath = _write_csv("X,Y,R,Lin,Lout,E\n" "500,2500,0,0,0,0,3340,660,1000,200,150,0.15,4340,5000,0,0,0,0\n")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ifcopenshell.api.alignment.create_from_csv(file, filepath)
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
test_create_from_csv()
|
||||||
|
test_create_from_csv_spiral_transitions()
|
||||||
|
test_create_from_csv_cant()
|
||||||
|
test_create_from_csv_cant_requires_transitions()
|
||||||
|
test_create_from_csv_cant_requires_vertical()
|
||||||
Reference in New Issue
Block a user