2
\$\begingroup\$

I'm calculating the injectors, and I've added the complete calculation code and tested it using pytest. Can you suggest a better way to structure the code so that I can easily add functions in the future and reuse entire classes for calculating two-component injectors?

jet_injector.py:

from abc import ABC
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from math import pi, exp, sqrt


@dataclass(frozen=True)
class Injector(ABC):
    density: float
    diameter: float
    length: float
    mass_flow_rate: float
    viscosity: float

    @cached_property
    def injector_nozzle_area(self) -> float:
        return pi * self.diameter**2 / 4

    @cached_property
    def reynolds_number(self) -> float:
        return 4 * self.mass_flow_rate / (pi * self.diameter * self.viscosity)

    @cached_property
    def relative_length_injector(self) -> float:
        return self.length / self.diameter


class Reynolds(Enum):
    LAMINAR = 2000
    TURBULENT = 10_000


@dataclass(frozen=True)
class LiquidJetInjector(Injector):
    density_comb: float
    sigma_fuel: float

    @cached_property
    def average_speed(self) -> float:
        return self.mass_flow_rate / (self.density * self.injector_nozzle_area)

    @cached_property
    def linear_hydraulic_resistance(self) -> float:
        if self.reynolds_number < Reynolds.LAMINAR.value:
            return 64 / self.reynolds_number
        elif Reynolds.LAMINAR.value <= self.reynolds_number <= Reynolds.TURBULENT.value:
            return 0.3164 * self.reynolds_number**-0.25
        return 0.031

    @cached_property
    def injector_losses_inlet(self) -> float:
        if self.reynolds_number < Reynolds.LAMINAR.value:
            return 2.2 - 0.726 * exp(
                -74.5 * self.viscosity * self.length / self.mass_flow_rate
            )
        return 1 + 2.65 * self.linear_hydraulic_resistance

    @cached_property
    def injector_flow_coefficient(self) -> float:
        return 1 / sqrt(
            self.injector_losses_inlet
            + self.linear_hydraulic_resistance * self.length / self.diameter
        )

    @cached_property
    def pressure_drop_injector(self) -> float:
        return self.mass_flow_rate**2 / (
            2
            * self.density
            * self.injector_flow_coefficient**2
            * self.injector_nozzle_area**2
        )

    @cached_property
    def weber_criterion(self) -> float:
        return (
            self.density_comb * self.average_speed**2 * self.diameter / self.sigma_fuel
        )

    @cached_property
    def media_diameter_spray_droplets(self) -> float:
        return self.diameter * (27 * pi / 4) ** (1 / 3) * self.weber_criterion ** (-1 / 3)


@dataclass(frozen=True)
class GasJetInjector(Injector):
    combustion_pressure: float
    pressure_drop_internal_circuit: float
    gas_constant_gen_gas: float
    temperature_gen_gas: float
    entropy_expansion_ratio: float

    @cached_property
    def injector_pressure(self) -> float:
        return self.combustion_pressure + self.pressure_drop_internal_circuit

    @cached_property
    def density_gen_gas(self) -> float:
        return self.injector_pressure / (
            self.gas_constant_gen_gas * self.temperature_gen_gas
        )

    @cached_property
    def average_speed(self) -> float:
        return self.mass_flow_rate / (self.density_gen_gas * self.injector_nozzle_area)

    @cached_property
    def injector_flow_coefficient(self) -> float:
        return ((sqrt(1.23 ** 2 + 232 * self.length / (self.reynolds_number * self.diameter)) - 1.23)
                / (116 * self.length / (self.reynolds_number * self.diameter)))

    @cached_property
    def injector_nozzle_area_outlet(self) -> float:
        return self.mass_flow_rate / (self.injector_flow_coefficient * self.density_gen_gas * (self.pressure_drop_internal_circuit / self.injector_pressure) ** (1 / self.entropy_expansion_ratio) * sqrt(2 * self.entropy_expansion_ratio / (self.entropy_expansion_ratio - 1) * self.gas_constant_gen_gas * self.temperature_gen_gas * (1 - (self.pressure_drop_internal_circuit / self.injector_pressure) ** ((self.entropy_expansion_ratio - 1) / self.entropy_expansion_ratio))))

    @cached_property
    def diameter_injector(self) -> float:
        return sqrt(4 * self.injector_nozzle_area_outlet / pi)

    @cached_property
    def discrepancy(self) -> float:
        return (self.diameter_injector - self.diameter) / self.diameter_injector

test_jet_injector.py:

import pytest
from src.fluxion.engine.jet_injector import LiquidJetInjector, GasJetInjector


class TestLiquidJetInjector:
    @pytest.fixture(scope="function")
    def liquid_jet_injector(self):
        liquid_injector = LiquidJetInjector(
            density=800,
            diameter=0.002,
            length=0.003,
            mass_flow_rate=0.05,
            viscosity=0.0015,
            density_comb=1.2,
            sigma_fuel=0.028,
        )
        return liquid_injector

    def test_injector_nozzle_area(self, liquid_jet_injector):
        assert liquid_jet_injector.injector_nozzle_area == pytest.approx(3.1415926535898e-06)

    def test_reynolds_number(self, liquid_jet_injector):
        assert liquid_jet_injector.reynolds_number == pytest.approx(21220.6590789194)

    def test_average_speed(self, liquid_jet_injector):
        assert liquid_jet_injector.average_speed == pytest.approx(1.9894367886487e+01)

    def test_relative_length_injector(self, liquid_jet_injector):
        assert liquid_jet_injector.relative_length_injector == pytest.approx(1.5)

    def test_linear_hydraulic_resistance(self, liquid_jet_injector):
        assert liquid_jet_injector.linear_hydraulic_resistance == pytest.approx(0.031)

    def test_injector_losses_inlet(self, liquid_jet_injector):
        assert liquid_jet_injector.injector_losses_inlet == pytest.approx(1.08215)

    def test_injector_flow_coefficient(self, liquid_jet_injector):
        assert liquid_jet_injector.injector_flow_coefficient == pytest.approx(0.941283307)

    def test_pressure_drop_injector(self, liquid_jet_injector):
        assert liquid_jet_injector.pressure_drop_injector == pytest.approx(1.7868149049676e+05)

    def test_weber_criterion(self, liquid_jet_injector):
        assert liquid_jet_injector.weber_criterion == pytest.approx(3.3924503451676e+01)

    def test_media_diameter_spray_droplets(self, liquid_jet_injector):
        assert liquid_jet_injector.media_diameter_spray_droplets == pytest.approx(1.71005486466707e-03)


class TestGasInjector:
    @pytest.fixture(scope="function")
    def gas_jet_injector(self):
        gas_injector = GasJetInjector(
            density=5.0,
            diameter=0.005,
            length=0.01,
            mass_flow_rate=0.1,
            viscosity=0.00002,
            combustion_pressure=5_000_000,
            pressure_drop_internal_circuit=500_000,
            gas_constant_gen_gas=300,
            temperature_gen_gas=800,
            entropy_expansion_ratio=1.2,
        )
        return gas_injector

    def test_injector_nozzle_area(self, gas_jet_injector):
        assert gas_jet_injector.injector_nozzle_area == pytest.approx(1.9634954084936e-05)

    def test_reynolds_number(self, gas_jet_injector):
        assert gas_jet_injector.reynolds_number == pytest.approx(1273239.5447351600)

    def test_average_speed(self, gas_jet_injector):
        assert gas_jet_injector.average_speed == pytest.approx(222.2381751)

    def test_relative_length_injector(self, gas_jet_injector):
        assert gas_jet_injector.relative_length_injector == pytest.approx(2)

    def test_injector_pressure(self, gas_jet_injector):
        assert gas_jet_injector.injector_pressure == pytest.approx(5500000)

    def test_density_gen_gas(self, gas_jet_injector):
        assert gas_jet_injector.density_gen_gas == pytest.approx(22.91666667)

    def test_injector_flow_coefficient(self, gas_jet_injector):
        assert gas_jet_injector.injector_flow_coefficient == pytest.approx(0.812959177)

    def test_injector_nozzle_area_outlet(self, gas_jet_injector):
        assert gas_jet_injector.injector_nozzle_area_outlet == pytest.approx(4.0646157686e-05)

    def test_diameter_injector(self, gas_jet_injector):
        assert gas_jet_injector.diameter_injector == pytest.approx(0.007193907)

    def test_discrepancy(self, gas_jet_injector):
        assert gas_jet_injector.discrepancy == pytest.approx(0.304967367)

This is the methodical manual by which everything was considered: training manual

\$\endgroup\$
2
  • \$\begingroup\$ Why have you made Injector a subclass of ABC or am I missing something? If there is a rationale for this, perhaps a comment would be appropriate. \$\endgroup\$ Commented Oct 19 at 10:58
  • 1
    \$\begingroup\$ I chuckled a bit - this is a second question about the same assignment from your uni, your ancestor has built something similar a year ago at codereview.stackexchange.com/questions/292474/… :) (I only remember that one because it was fun to review code without any understanding of the underlying theory, just hoping the formulae are correct) \$\endgroup\$ Commented Oct 21 at 13:40

2 Answers 2

2
\$\begingroup\$

Overview

The code layout is good, and you used meaningful names for classes, functions and variables. It seems like the code is already structured for future enhancements.

Long lines

There are a couple of really long lines, and that can reduce the code readability. You were meticulous about keeping most of the lines short by splitting a single long statement across several lines. You should do the same with this line:

return self.mass_flow_rate / (self.injector_flow_coefficient * self.density_gen_gas * (self.pressure_drop_internal_circuit / self.injector_pressure) ** (1 / self.entropy_expansion_ratio) * sqrt(2 * self.entropy_expansion_ratio / (self.entropy_expansion_ratio - 1) * self.gas_constant_gen_gas * self.temperature_gen_gas * (1 - (self.pressure_drop_internal_circuit / self.injector_pressure) ** ((self.entropy_expansion_ratio - 1) / self.entropy_expansion_ratio))))

Documentation

The PEP 8 style guide recommends adding docstrings for classes and functions.

Magic numbers

There are several numeric constants that could be explained with docstrings, comments or named identifiers. For example:

return 2.2 - 0.726 * exp(
    -74.5 * self.viscosity * self.length / self.mass_flow_rate
)

You could make a note if this is a well-known equation.

Simpler

In the linear_hydraulic_resistance function, this elif:

elif Reynolds.LAMINAR.value <= self.reynolds_number <= Reynolds.TURBULENT.value:

is more typically written as an if since the previous if does a return:

if Reynolds.LAMINAR.value <= self.reynolds_number <= Reynolds.TURBULENT.value:
\$\endgroup\$
3
\$\begingroup\$

I don't really understand any of this, so I can only speak as a programmer, but the code looks good and fairly well structured. Naming conventions make sense in plain English even to an outsider.

If accuracy is important you might want to use the fractions module. This would improve accuracy and eliminate rounding errors, so that your tests no longer have to rely on approximations.

Besides, you seem to be relying on default pytest.approx tolerance levels. Depending on the values you are handling this might not always be appropriate, check the docs to be sure.

Thus, return 64 / self.reynolds_number where reynolds_number = 1500 could be expressed as return Fraction(64, self.reynolds_number), which returns 16/375, that is equal to the 0.042666666666666665 that your function returns.

This may not be a concern here but there is a potential pitfall about cached_property to be aware of (docs):

The cached_property does not prevent a possible race condition in multi-threaded usage. The getter function could run more than once on the same instance, with the latest run setting the cached value. If the cached property is idempotent or otherwise not harmful to run more than once on an instance, this is fine. If synchronization is needed, implement the necessary locking inside the decorated getter function or around the cached property access.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.