diff --git a/ditto/readers/synergi/length_units.py b/ditto/readers/synergi/length_units.py new file mode 100644 index 00000000..59c81fb7 --- /dev/null +++ b/ditto/readers/synergi/length_units.py @@ -0,0 +1,93 @@ +from enum import Enum + + +class SynergiValueType(Enum): + SUL = 'SUL' + MUL = 'MUL' + LUL = 'LUL' + Per_LUL = 'Per_LUL' + + +def convert_length_unit(value, value_type, length_units): + """ + Converts a Synergi value to an SI type. + + The value is interpreted based on: + +----------+----------------+------------+ + |value_type| length_units | Unit | + +==========+================+============+ + | SUL | English2 | inch | + +----------+----------------+------------+ + | SUL | English1 | inch | + +----------+----------------+------------+ + | SUL | Metric | mm | + +----------+----------------+------------+ + | MUL | English2 | foot | + +----------+----------------+------------+ + | MUL | English1 | foot | + +----------+----------------+------------+ + | MUL | Metric | m | + +----------+----------------+------------+ + | LUL | English2 | mile | + +----------+----------------+------------+ + | LUL | English1 | kft | + +----------+----------------+------------+ + | LUL | Metric | km | + +----------+----------------+------------+ + | Per_LUL | English2 | per mile | + +----------+----------------+------------+ + | Per_LUL | English1 | per kft | + +----------+----------------+------------+ + | Per_LUL | Metric | per km | + +----------+----------------+------------+ + + The return value is based on + + SUL: metres + MUL: metres + LUL: metres + Per_LUL: per metre + """ + + if not isinstance(value_type, SynergiValueType): + raise ValueError( + 'convert_length_unit received an invalid value_type value' + ' of {}'.format(value_type) + ) + + if not isinstance(length_units, str): + raise ValueError( + 'convert_length_unit must be passed a string length_units' + ' parameter. {} was received.'.format(length_units) + ) + + if length_units not in {'English2', 'English1', 'Metric'}: + raise ValueError( + 'convert_length_unit received an invalid length unit {}'.format( + length_units + ) + ) + + CONVERSION_FACTORS = { + 'English2': { + SynergiValueType.SUL: 0.0254, + SynergiValueType.MUL: 0.3048, + SynergiValueType.LUL: 1609.34, + SynergiValueType.Per_LUL: 1/1609.34, + }, + 'English1': { + SynergiValueType.SUL: 0.0254, + SynergiValueType.MUL: 0.3048, + SynergiValueType.LUL: 304.8, + SynergiValueType.Per_LUL: 3.28084 * 10 ** -3, + }, + 'Metric': { + SynergiValueType.SUL: 10 ** -3, + SynergiValueType.MUL: 1.0, + SynergiValueType.LUL: 1e3, + SynergiValueType.Per_LUL: 10 ** -3, + } + } + + factor = CONVERSION_FACTORS[length_units][value_type] + return value * factor diff --git a/ditto/readers/synergi/read.py b/ditto/readers/synergi/read.py index 453a181d..76142755 100644 --- a/ditto/readers/synergi/read.py +++ b/ditto/readers/synergi/read.py @@ -33,6 +33,11 @@ from ditto.models.position import Position from ditto.models.base import Unicode +from ditto.readers.synergi.length_units import ( + convert_length_unit, + SynergiValueType, +) + logger = logging.getLogger(__name__) @@ -625,7 +630,11 @@ def parse(self, model): # Assumes MUL is medium unit length and this is feets # Converts to meters then # - api_line.length = LineLength[i] * 0.3048 + api_line.length = convert_length_unit( + LineLength[i], + SynergiValueType.MUL, + LengthUnits + ) # From element # Replace spaces with "_" @@ -851,12 +860,6 @@ def parse(self, model): # The Neutral will be handled seperately if phase != "N": - - # Assumes MUL is medium unit length = ft - # Convert to meters - # - coeff = 0.3048 - # Set the position of the first wire if ( idx == 0 @@ -865,15 +868,19 @@ def parse(self, model): and "Position1_Y_MUL" in config ): # Set X - api_wire.X = ( - config["Position1_X_MUL"] * coeff - ) # DiTTo is in meters + api_wire.X = convert_length_unit( + config["Position1_X_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set Y # Add the reference height - api_wire.Y = ( - AveHeightAboveGround_MUL[i] + config["Position1_Y_MUL"] - ) * coeff # DiTTo is in meters + api_wire.Y = convert_length_unit( + AveHeightAboveGround_MUL[i] + config["Position1_Y_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set the position of the second wire if ( @@ -883,15 +890,19 @@ def parse(self, model): and "Position2_Y_MUL" in config ): # Set X - api_wire.X = ( - config["Position2_X_MUL"] * coeff - ) # DiTTo is in meters + api_wire.X = convert_length_unit( + config["Position2_X_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set Y # Add the reference height - api_wire.Y = ( - AveHeightAboveGround_MUL[i] + config["Position2_Y_MUL"] - ) * coeff # DiTTo is in meters + api_wire.Y = convert_length_unit( + AveHeightAboveGround_MUL[i] + config["Position2_Y_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set the position of the third wire if ( @@ -901,15 +912,19 @@ def parse(self, model): and "Position3_Y_MUL" in config ): # Set X - api_wire.X = ( - config["Position3_X_MUL"] * coeff - ) # DiTTo is in meters + api_wire.X = convert_length_unit( + config["Position3_X_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set Y # Add the reference height - api_wire.Y = ( - AveHeightAboveGround_MUL[i] + config["Position3_Y_MUL"] - ) * coeff # DiTTo is in meters + api_wire.Y = convert_length_unit( + AveHeightAboveGround_MUL[i] + config["Position3_Y_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set the characteristics of the first wire. Use PhaseConductorID # @@ -997,24 +1012,22 @@ def parse(self, model): conductor_name_raw = NeutralConductorID[i] # Set the Spacing of the neutral - # - # Assumes MUL is medium unit length = ft - # Convert to meters - # - coeff = 0.3048 - if "Neutral_X_MUL" in config and "Neutral_Y_MUL" in config: # Set X - api_wire.X = ( - config["Neutral_X_MUL"] * coeff + api_wire.X = convert_length_unit( + config["Neutral_X_MUL"], + SynergiValueType.MUL, + LengthUnits ) # DiTTo is in meters # Set Y # Add the reference height - api_wire.Y = ( - AveHeightAboveGround_MUL[i] + config["Neutral_Y_MUL"] - ) * coeff # DiTTo is in meters + api_wire.Y = convert_length_unit( + AveHeightAboveGround_MUL[i] + config["Neutral_Y_MUL"], + SynergiValueType.MUL, + LengthUnits + ) # Set the characteristics of the wire: # - GMR @@ -1030,16 +1043,19 @@ def parse(self, model): # Set the GMR of the conductor # DiTTo is in meters and GMR is assumed to be given in feets # - api_wire.gmr = ( - conductor_mapping[conductor_name_raw]["CableGMR"] * 0.3048 + api_wire.gmr = convert_length_unit( + conductor_mapping[conductor_name_raw]["CableGMR"], + SynergiValueType.MUL, + LengthUnits ) # Set the Diameter of the conductor # Diameter is assumed to be given in inches and is converted to meters here # - api_wire.diameter = ( - conductor_mapping[conductor_name_raw]["CableDiamOutside"] - * 0.0254 + api_wire.diameter = convert_length_unit( + conductor_mapping[conductor_name_raw]["CableDiamOutside"], + SynergiValueType.SUL, + LengthUnits ) # Set the Ampacity of the conductor @@ -1062,11 +1078,11 @@ def parse(self, model): # TODO: Change this once resistance is the per unit length resistance # if api_line.length is not None: - api_wire.resistance = ( + api_wire.resistance = convert_length_unit( conductor_mapping[conductor_name_raw]["CableResistance"] - * api_line.length - * 1.0 - / 1609.34 + * api_line.length, + SynergiValueType.Per_LUL, + LengthUnits ) # Add the new Wire to the line's list of wires @@ -1110,29 +1126,16 @@ def parse(self, model): # | Z0-Z+ Z0-Z+ Z0+2*Z+ | # -------------------------- - # TODO: Check that the following is correct... - # If LengthUnits is set to English2 or not defined , then assume miles - if LengthUnits == "English2" or LengthUnits is None: - coeff = 0.000621371 - # Else, if LengthUnits is set to English1, assume kft - elif LengthUnits == "English1": - coeff = 3.28084 * 10 ** -3 - # Else, if LengthUnits is set to Metric, assume km - elif LengthUnits == "Metric": - coeff = 10 ** -3 - else: - raise ValueError( - "LengthUnits <{}> is not valid.".format(LengthUnits) - ) - - # Multiply by 1/3 - coeff *= 1.0 / 3.0 + r0 = convert_length_unit(r0, SynergiValueType.Per_LUL, LengthUnits) / 3.0 + r1 = convert_length_unit(r1, SynergiValueType.Per_LUL, LengthUnits) / 3.0 + x0 = convert_length_unit(x0, SynergiValueType.Per_LUL, LengthUnits) / 3.0 + x1 = convert_length_unit(x1, SynergiValueType.Per_LUL, LengthUnits) / 3.0 # One phase case (One phase + neutral) # if NPhase == 2: impedance_matrix = [ - [coeff * complex(float(r0) + float(r1), float(x0) + float(x1))] + [complex(float(r0) + float(r1), float(x0) + float(x1))] ] # Two phase case (Two phases + neutral) @@ -1151,9 +1154,9 @@ def parse(self, model): if b2 == 0: b2 = float(x1) - b = coeff * complex(b1, b2) + b = complex(b1, b2) - a = coeff * complex( + a = complex( (2 * float(r1) + float(r0)), (2 * float(x1) + float(x0)) ) @@ -1162,7 +1165,7 @@ def parse(self, model): # Three phases case (Three phases + neutral) # if NPhase == 4: - a = coeff * complex( + a = complex( (2 * float(r1) + float(r0)), (2 * float(x1) + float(x0)) ) b1 = float(r0) - float(r1) @@ -1177,7 +1180,7 @@ def parse(self, model): if b2 == 0: b2 = float(x1) - b = coeff * complex(b1, b2) + b = complex(b1, b2) impedance_matrix = [[a, b, b], [b, a, b], [b, b, a]] diff --git a/tests/readers/synergi/__init__.py b/tests/readers/synergi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/readers/synergi/test_unit_converter.py b/tests/readers/synergi/test_unit_converter.py new file mode 100644 index 00000000..411e8c08 --- /dev/null +++ b/tests/readers/synergi/test_unit_converter.py @@ -0,0 +1,50 @@ +import pytest + +from ditto.readers.synergi.length_units import ( + convert_length_unit, + SynergiValueType, +) + + +@pytest.mark.parametrize( + 'value, value_type, length_unit, expected', + [ + (39.3701, SynergiValueType.SUL, 'English2', pytest.approx(1.0)), + (3.28084, SynergiValueType.MUL, 'English2', pytest.approx(1.0)), + (1.0, SynergiValueType.LUL, 'English2', pytest.approx(1609.34)), + ( + 1609.34, + SynergiValueType.Per_LUL, + 'English2', + pytest.approx(1.0, abs=1e-5) + ), + + (39.3701, SynergiValueType.SUL, 'English1', pytest.approx(1.0)), + (3.28084, SynergiValueType.MUL, 'English1', pytest.approx(1.0)), + (1.0, SynergiValueType.LUL, 'English1', pytest.approx(304.8)), + (304.8, SynergiValueType.Per_LUL, 'English1', pytest.approx(1.0)), + + (1000.0, SynergiValueType.SUL, 'Metric', 1.0), + (2.0, SynergiValueType.MUL, 'Metric', 2.0), + (1.0, SynergiValueType.LUL, 'Metric', 1000.0), + (1000.0, SynergiValueType.Per_LUL, 'Metric', 1.0), + ] +) +def test_convert_units(value, value_type, length_unit, expected): + actual = convert_length_unit(value, value_type, length_unit) + assert actual == expected + + +def test_raises_error_invalid_value_type(): + with pytest.raises(ValueError): + convert_length_unit(1.0, 'a', 'English2') + + +def test_raises_error_invalid_unit_type(): + with pytest.raises(ValueError): + convert_length_unit(1.0, SynergiValueType.SUL, 1) + + +def test_raises_error_invalid_unit_value(): + with pytest.raises(ValueError): + convert_length_unit(1.0, SynergiValueType.SUL, 'English3')