Tests

Tests: test_aux_tools

  1'''Unit tests for fluidsolve.aux_tools.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring
  5
  6import inspect
  7import pytest
  8import fluidsolve.aux_tools as module_under_test
  9
 10def test_module_importable() -> None:
 11  assert module_under_test is not None
 12
 13@pytest.mark.parametrize('name', ['GetArgs', 'vFun'])
 14def test_public_classes_exist(name: str) -> None:
 15  obj = getattr(module_under_test, name)
 16  assert inspect.isclass(obj)
 17
 18@pytest.mark.parametrize('name', ['getPumpCurveDataText', 'prepareArgs', 'spec', 'toUnits'])
 19def test_public_functions_are_callable(name: str) -> None:
 20  obj = getattr(module_under_test, name)
 21  assert callable(obj)
 22
 23def test_toUnits_scalar_to_quantity() -> None:
 24  q = module_under_test.toUnits(2, module_under_test.u.m)
 25  assert q.magnitude == 2
 26  assert q.units == module_under_test.u.m
 27
 28def test_toUnits_quantity_conversion_and_magnitude() -> None:
 29  value = 250 * module_under_test.u.cm
 30  out = module_under_test.toUnits(value, module_under_test.u.m, magnitude=True)
 31  assert out == 2.5
 32
 33def test_toUnits_none_value_raises() -> None:
 34  with pytest.raises(ValueError, match='Value is None'):
 35    module_under_test.toUnits(None, module_under_test.u.m)  # type: ignore[arg-type]
 36
 37def test_toUnits_returns_value_unchanged_when_units_none() -> None:
 38  value = 5
 39  assert module_under_test.toUnits(value, None) == value  # type: ignore[arg-type]
 40
 41def test_prepareArgs_filters_none_values() -> None:
 42  args = module_under_test.prepareArgs(a=1, b=None, c='x')
 43  assert args == {'a': 1, 'c': 'x'}
 44
 45def test_getPumpCurveDataText_parses_whitespace_and_commas() -> None:
 46  data = '''
 47    1, 2
 48    3 4
 49  '''
 50  assert module_under_test.getPumpCurveDataText(data) == [1.0, 2.0, 3.0, 4.0]
 51
 52def test_getPumpCurveDataText_empty_string_returns_empty_list() -> None:
 53  assert module_under_test.getPumpCurveDataText('') == []
 54
 55def test_spec_returns_input_kwargs_as_dict() -> None:
 56  out = module_under_test.spec(comp='Tube', nodes=['A', 'B'], sense=-1, D=50)
 57  assert out == {'comp': 'Tube', 'nodes': ['A', 'B'], 'sense': -1, 'D': 50}
 58
 59def test_getargs_with_validators_and_remove() -> None:
 60  args = module_under_test.GetArgs({'name': '  abc  '})
 61  result = args.getArg('name', [module_under_test.vFun.stripspaces(), module_under_test.vFun.toupper()])
 62  assert result == 'ABC'
 63  assert args.restArgs() == {}
 64
 65def test_getargs_init_rejects_non_dict() -> None:
 66  with pytest.raises(TypeError, match='is not a dict'):
 67    module_under_test.GetArgs([('name', 'abc')])  # type: ignore[arg-type]
 68
 69def test_getargs_getarg_keeps_value_when_remove_false() -> None:
 70  args = module_under_test.GetArgs({'name': 'abc'})
 71  result = args.getArg('name', remove=False)
 72  assert result == 'abc'
 73  assert args.restArgs() == {'name': 'abc'}
 74
 75def test_getargs_invalid_name_type_raises() -> None:
 76  args = module_under_test.GetArgs({'name': 'abc'})
 77  with pytest.raises(TypeError, match='name argument 1 is not a str'):
 78    args.getArg(1)  # type: ignore[arg-type]
 79
 80def test_getargs_invalid_validators_type_raises() -> None:
 81  args = module_under_test.GetArgs({'name': 'abc'})
 82  with pytest.raises(TypeError, match='validators argument .* is not a list'):
 83    args.getArg('name', module_under_test.vFun.stripspaces())  # type: ignore[arg-type]
 84
 85def test_getargs_non_function_validator_raises() -> None:
 86  args = module_under_test.GetArgs({'name': 'abc'})
 87  with pytest.raises(TypeError, match='is not a function'):
 88    args.getArg('name', ['not-a-function'])  # type: ignore[list-item]
 89
 90def test_getargs_addarg_and_addargs_preserve_existing_values() -> None:
 91  args = module_under_test.GetArgs({'a': 1})
 92  args.addArg('b', 2)
 93  args.addArgs({'b': 99, 'c': 3})
 94  assert args.restArgs() == {'a': 1, 'b': 2, 'c': 3}
 95
 96def test_getargs_isempty_behaviour_matches_current_contract() -> None:
 97  empty_args = module_under_test.GetArgs({})
 98  assert empty_args.isEmpty() is False
 99
100  remaining_args = module_under_test.GetArgs({'left': 1})
101  assert remaining_args.isEmpty(raiseerror=False) is True
102  with pytest.raises(TypeError, match='argument left'):
103    remaining_args.isEmpty()
104
105def test_getargs_default_when_missing() -> None:
106  args = module_under_test.GetArgs({})
107  result = args.getArg('speed', [module_under_test.vFun.default(2900)])
108  assert result == 2900
109
110def test_getargs_missing_without_default_raises() -> None:
111  args = module_under_test.GetArgs({})
112  with pytest.raises(ValueError, match='Name speed not found'):
113    args.getArg('speed', [module_under_test.vFun.istype(int)])
114
115def test_vfun_istype_rejects_wrong_type() -> None:
116  validator = module_under_test.vFun.istype(int)
117  with pytest.raises(ValueError, match='not of type'):
118    validator('n', '3')
119
120def test_vfun_totype_casts_and_respects_optional_none() -> None:
121  validator = module_under_test.vFun.totype(int)
122  assert validator('n', '3') == 3
123  optional_validator = module_under_test.vFun.totype(int, need=False)
124  assert optional_validator('n', None) is None
125
126def test_vfun_case_converters_and_stripspaces_allow_optional_none() -> None:
127  assert module_under_test.vFun.stripspaces()('name', '  a b  ') == 'a b'
128  assert module_under_test.vFun.tolower()('name', 'AbC') == 'abc'
129  assert module_under_test.vFun.toupper(need=False)('name', None) is None
130
131def test_vfun_tounits_converts_and_handles_none_modes() -> None:
132  validator = module_under_test.vFun.tounits(module_under_test.u.cm, magnitude=True)
133  assert validator('length', 2) == 2
134  with pytest.raises(ValueError, match='Argument is None'):
135    module_under_test.vFun.tounits(module_under_test.u.cm)('length', None)
136  assert module_under_test.vFun.tounits(module_under_test.u.cm, need=False)('length', None) is None
137
138def test_vfun_sanitizefilepath_and_tolambda_transform() -> None:
139  validator = module_under_test.vFun.sanitizefilepath()
140  assert validator('path', 'a/../b\\file.txt') == module_under_test.os.path.normpath('a/../b\\file.txt')
141  lambda_validator = module_under_test.vFun.tolambda(lambda value: value * 2)
142  assert lambda_validator('n', 4) == 8
143  assert module_under_test.vFun.tolambda(lambda value: value, need=False)('n', None) is None
144
145def test_vfun_istype_accepts_tuple_and_custom_message() -> None:
146  validator = module_under_test.vFun.istype((int, float))
147  assert validator('n', 3.5) == 3.5
148  with pytest.raises(ValueError, match='bad type'):
149    module_under_test.vFun.istype(int, errmsg='bad type')('n', '3')
150
151def test_vfun_notnone_and_notempty_raise_custom_messages() -> None:
152  with pytest.raises(ValueError, match='required'):
153    module_under_test.vFun.notnone(errmsg='required')('name', None)
154  with pytest.raises(ValueError, match='empty'):
155    module_under_test.vFun.notempty(errmsg='empty')('name', '')
156
157def test_vfun_length_validators_cover_success_and_failure() -> None:
158  assert module_under_test.vFun.haslen(3)('name', 'abc') == 'abc'
159  with pytest.raises(ValueError, match='length 2 not equal to 3'):
160    module_under_test.vFun.haslen(3)('name', 'ab')
161
162  assert module_under_test.vFun.lenmax(3)('name', 'abc') == 'abc'
163  with pytest.raises(ValueError, match='more than max 3'):
164    module_under_test.vFun.lenmax(3)('name', 'abcd')
165
166  assert module_under_test.vFun.lenmin(2)('name', 'abc') == 'abc'
167  with pytest.raises(ValueError, match='less than min 2'):
168    module_under_test.vFun.lenmin(2)('name', 'a')
169  assert module_under_test.vFun.lenmin(2, need=False)('name', None) is None
170
171def test_vfun_inrange_current_behaviour_and_optional_none() -> None:
172  validator = module_under_test.vFun.inrange(1, 5)
173  assert validator('n', 3) == 3
174  with pytest.raises(ValueError, match='must be between 1 to 5'):
175    validator('n', 7)
176  assert module_under_test.vFun.inrange(1, 5, need=False)('n', None) is None
177
178def test_vfun_inlist_allows_and_rejects() -> None:
179  validator = module_under_test.vFun.inlist('a', 'b', 'c')
180  assert validator('k', 'b') == 'b'
181  with pytest.raises(ValueError, match='must be one of'):
182    validator('k', 'z')
183
184def test_vfun_inlist_supports_tuple_input_and_inverse() -> None:
185  validator = module_under_test.vFun.inlist(('a', 'b'))
186  assert validator('k', 'a') == 'a'
187  inverse_validator = module_under_test.vFun.inlist('x', 'y', inv=True)
188  assert inverse_validator('k', 'z') == 'z'
189  with pytest.raises(ValueError, match='may not be one of x,y'):
190    inverse_validator('k', 'x')
191
192def test_vfun_islambda_supports_callable_and_optional_none() -> None:
193  assert module_under_test.vFun.islambda(lambda value: value > 0)('n', 5) == 5
194  assert module_under_test.vFun.islambda(lambda value: value > 0, need=False)('n', None) is None
195  with pytest.raises(ValueError, match='must be positive'):
196    module_under_test.vFun.islambda(lambda value: value > 0, errmsg='must be positive')('n', -1)
197  with pytest.raises(TypeError, match='condition must be callable'):
198    module_under_test.vFun.islambda(True)('n', 1)
199
200def test_vfun_regex_inv_true_requires_match() -> None:
201  validator = module_under_test.vFun.regex(r'^[A-Z]+$', inv=True)
202  assert validator('code', 'ABC') == 'ABC'
203  with pytest.raises(ValueError, match='must conform to regex'):
204    validator('code', 'Abc')
205
206def test_vfun_regex_default_mode_rejects_matching_values_and_invalid_pattern() -> None:
207  validator = module_under_test.vFun.regex(r'^[A-Z]+$')
208  assert validator('code', 'Abc') == 'Abc'
209  with pytest.raises(ValueError, match='may not conform to regex'):
210    validator('code', 'ABC')
211  with pytest.raises(ValueError, match='is not a valid regex'):
212    module_under_test.vFun.regex('[')
213
214def test_vfun_fileexists(tmp_path) -> None:
215  file_path = tmp_path / 'x.txt'
216  file_path.write_text('ok', encoding='utf-8')
217  validator = module_under_test.vFun.fileexists()
218  assert validator('file', str(file_path)) == str(file_path)
219  with pytest.raises(ValueError, match='does not exist'):
220    validator('file', str(file_path.with_name('missing.txt')))
221
222def test_vfun_file_access_validators_support_optional_none_and_existing_files(tmp_path) -> None:
223  file_path = tmp_path / 'x.txt'
224  file_path.write_text('ok', encoding='utf-8')
225
226  assert module_under_test.vFun.fileexists(need=False)('file', None) is None
227  assert module_under_test.vFun.isfilereadable()('file', str(file_path)) == str(file_path)
228  assert module_under_test.vFun.isfilewritable()('file', str(file_path)) == str(file_path)
229  assert module_under_test.vFun.isfileexecutable(need=False)('file', None) is None
230
231def test_vfun_file_access_validators_raise_for_missing_files(tmp_path) -> None:
232  missing = str(tmp_path / 'missing.txt')
233  with pytest.raises(ValueError, match='missing'):
234    module_under_test.vFun.isfilereadable(errmsg='missing')('file', missing)
235  with pytest.raises(ValueError, match='missing'):
236    module_under_test.vFun.isfilewritable(errmsg='missing')('file', missing)
237  with pytest.raises(ValueError, match='missing'):
238    module_under_test.vFun.isfileexecutable(errmsg='missing')('file', missing)

Tests: test_catalogue

  1'''Behavioral unit tests for fluidsolve.catalogue.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring
  5
  6import inspect
  7import json
  8import pytest
  9import fluidsolve.catalogue as module_under_test
 10
 11def test_module_importable() -> None:
 12  assert module_under_test is not None
 13
 14@pytest.mark.parametrize('name', ['Catalogue'])
 15def test_public_classes_exist(name: str) -> None:
 16  obj = getattr(module_under_test, name)
 17  assert inspect.isclass(obj)
 18
 19@pytest.mark.parametrize('name', ['Quantity', 'u'])
 20def test_public_variables_exist(name: str) -> None:
 21  assert hasattr(module_under_test, name)
 22
 23def test_catalogue_loads_builtin_libraries() -> None:
 24  cat = module_under_test.Catalogue(load=True)
 25  libs = cat.findLibraries()
 26  assert len(libs) > 0
 27  assert 'PUMP:C:APV' in libs
 28  assert 'PIPE:NW' in libs
 29
 30def test_find_libraries_matchcase_false() -> None:
 31  cat = module_under_test.Catalogue(load=True)
 32  libs = cat.findLibraries('apv', matchcase=False)
 33  assert 'PUMP:C:APV' in libs
 34
 35def test_find_libraries_with_expression() -> None:
 36  cat = module_under_test.Catalogue(load=True)
 37  libs = cat.findLibraries('pump AND centrifugal')
 38  assert 'PUMP:C:APV' in libs
 39
 40def test_search_in_library_with_numeric_and_string_criteria() -> None:
 41  cat = module_under_test.Catalogue(load=True)
 42  records = cat.searchInLibrary(
 43    'PUMP:C:APV',
 44    'T = centrifugal AND spec = "W+ 22/20" AND impeller0 = 110 AND speed0 = 2900',
 45  )
 46  assert len(records) >= 1
 47  for rec in records:
 48    assert rec['T'] == 'centrifugal'
 49    assert rec['spec'] == 'W+ 22/20'
 50    assert rec['impeller0'] == 110
 51    assert rec['speed0'] == 2900
 52
 53def test_search_in_library_list_input() -> None:
 54  cat = module_under_test.Catalogue(load=True)
 55  records = cat.searchInLibrary(['PIPE:NW'], 'OD < 20')
 56  dns = {rec['DN'] for rec in records}
 57  assert 'DN10' in dns
 58  assert 'DN15' in dns
 59
 60def test_parse_expression_builds_tree_for_and_or() -> None:
 61  cat = module_under_test.Catalogue(load=False)
 62  parsed = cat._parseExpression(['A', 'AND', '(', 'B', 'OR', 'C', ')'])  # pylint: disable=protected-access
 63  assert 'AND' in parsed
 64  assert parsed['AND'][0] == 'A'
 65  assert 'OR' in parsed['AND'][1]
 66
 67def test_eval_record_expression_supports_not() -> None:
 68  cat = module_under_test.Catalogue(load=False)
 69  expr = {'NOT': 'OD < 10'}
 70  rec = {'OD': 13.0}
 71  assert cat._evalRecExpression(expr, rec) is True  # pylint: disable=protected-access
 72
 73def test_load_data_from_custom_path_only(tmp_path) -> None:
 74  data = {
 75    'library': {
 76      'name': 'TEST:LIB',
 77      'keywords': ['demo', 'custom'],
 78      'norm': [],
 79    },
 80    'keys': ['k', 'v'],
 81    'records': [{'k': 'x', 'v': 1}],
 82  }
 83  file_path = tmp_path / 'test_lib.json'
 84  file_path.write_text(json.dumps(data), encoding='utf-8')
 85  cat = module_under_test.Catalogue(path=str(tmp_path), load=False)
 86  cat.loadAllData(buildin=False)
 87  libs = cat.findLibraries()
 88  assert libs == ['TEST:LIB']
 89
 90def test_catalogue_init_normalizes_path_string_to_single_item_list(tmp_path) -> None:
 91  cat = module_under_test.Catalogue(path=str(tmp_path), load=False)
 92  assert cat._path == [str(tmp_path)]  # pylint: disable=protected-access
 93
 94def test_load_all_data_reports_invalid_json_and_skips_file(tmp_path, capsys) -> None:
 95  valid_data = {
 96    'library': {'name': 'VALID:LIB', 'keywords': ['valid'], 'norm': []},
 97    'keys': ['name'],
 98    'records': [{'name': 'demo'}],
 99  }
100  (tmp_path / 'valid.json').write_text(json.dumps(valid_data), encoding='utf-8')
101  (tmp_path / 'broken.json').write_text('{not valid json', encoding='utf-8')
102  cat = module_under_test.Catalogue(path=str(tmp_path), load=False)
103  cat.loadAllData(buildin=False)
104  captured = capsys.readouterr()
105  assert 'Error decoding JSON from file' in captured.out
106  assert cat.findLibraries() == ['VALID:LIB']
107
108def test_find_libraries_empty_criteria_returns_all_loaded_keys() -> None:
109  cat = module_under_test.Catalogue(load=False)
110  cat._d = {  # pylint: disable=protected-access
111    'LIB:A': {'library': {'name': 'LIB:A', 'keywords': ['pump'], 'norm': []}, 'records': []},
112    'LIB:B': {'library': {'name': 'LIB:B', 'keywords': ['valve'], 'norm': []}, 'records': []},
113  }
114  assert cat.findLibraries() == ['LIB:A', 'LIB:B']
115
116def test_find_libraries_supports_not_and_wildcards() -> None:
117  cat = module_under_test.Catalogue(load=False)
118  cat._d = {  # pylint: disable=protected-access
119    'PUMP:C:APV': {'library': {'name': 'PUMP:C:APV', 'keywords': ['pump', 'centrifugal', 'APV'], 'norm': []}, 'records': []},
120    'VALVE:C:GEN': {'library': {'name': 'VALVE:C:GEN', 'keywords': ['valve', 'control'], 'norm': []}, 'records': []},
121  }
122  assert cat.findLibraries('pump AND A*') == ['PUMP:C:APV']
123  assert cat.findLibraries('NOT valve') == ['PUMP:C:APV']
124
125def test_search_in_library_respects_matchcase_for_record_strings() -> None:
126  cat = module_under_test.Catalogue(load=False)
127  cat._d = {  # pylint: disable=protected-access
128    'TEST:LIB': {
129      'library': {'name': 'TEST:LIB', 'keywords': ['demo'], 'norm': []},
130      'records': [
131        {'kind': 'Pump', 'dn': 20},
132        {'kind': 'Valve', 'dn': 25},
133      ],
134    },
135  }
136  assert cat.searchInLibrary('TEST:LIB', 'kind = pump', matchcase=False) == [{'kind': 'Pump', 'dn': 20}]
137  assert not cat.searchInLibrary('TEST:LIB', 'kind = pump', matchcase=True)
138
139def test_parse_expression_keeps_quoted_values_with_spaces_together() -> None:
140  cat = module_under_test.Catalogue(load=False)
141  parsed = cat._parseExpression(['spec', '=', '"W+', '22/20"', 'AND', 'speed0', '=', '2900'])  # pylint: disable=protected-access
142  assert parsed == {'AND': ['spec = "W+ 22/20"', 'speed0 = 2900']}
143
144def test_parse_expression_unclosed_quote_raises_value_error() -> None:
145  cat = module_under_test.Catalogue(load=False)
146  with pytest.raises(ValueError, match='Unclosed quoted value'):
147    cat._parseExpression(['spec', '=', '"W+'])  # pylint: disable=protected-access
148
149def test_eval_library_expression_raises_for_invalid_expression_shape() -> None:
150  cat = module_under_test.Catalogue(load=False)
151  with pytest.raises(ValueError, match='Invalid expression format'):
152    cat._evalLibExpression({'XOR': ['pump', 'valve']}, ['pump'])  # pylint: disable=protected-access
153
154def test_eval_record_expression_supports_comparisons_and_missing_fields() -> None:
155  cat = module_under_test.Catalogue(load=False)
156  rec = {'dn': 25, 'kind': 'Valve'}
157  assert cat._evalRecExpression('dn >= 20', rec) is True  # pylint: disable=protected-access
158  assert cat._evalRecExpression('kind != Pump', rec) is True  # pylint: disable=protected-access
159  assert cat._evalRecExpression('missing = x', rec) is False  # pylint: disable=protected-access
160
161def test_eval_record_expression_raises_for_invalid_inputs() -> None:
162  cat = module_under_test.Catalogue(load=False)
163  with pytest.raises(ValueError, match='Invalid atomic criterion'):
164    cat._evalRecExpression('invalid criterion', {'dn': 25})  # pylint: disable=protected-access
165  with pytest.raises(ValueError, match='Invalid expression format'):
166    cat._evalRecExpression({'XOR': ['dn > 10', 'dn < 30']}, {'dn': 25})  # pylint: disable=protected-access

Tests: test_comp_base

  1'''Auto-generated tests for fluidsolve.comp_base.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring
  5
  6import inspect
  7import types
  8import pytest
  9import fluidsolve.comp_base as module_under_test
 10
 11def test_module_importable() -> None:
 12  assert module_under_test is not None
 13
 14@pytest.mark.parametrize('name', ['Comp_Base', 'Comp_Dummy', 'Comp_Reverse'])
 15def test_public_classes_exist(name: str) -> None:
 16  obj = getattr(module_under_test, name)
 17  assert inspect.isclass(obj)
 18
 19def test_public_functions_are_callable() -> None:
 20  public_function_names = [
 21    name
 22    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 23    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 24  ]
 25  for name in public_function_names:
 26    obj = getattr(module_under_test, name)
 27    assert callable(obj)
 28
 29@pytest.mark.parametrize('name', ['NO_DIAMETER', 'NO_LENGTH', 'NO_MEDIUM', 'Quantity', 'u'])
 30def test_public_variables_exist(name: str) -> None:
 31  assert hasattr(module_under_test, name)
 32
 33def test_comp_base_defaults_and_properties() -> None:
 34  comp = module_under_test.Comp_Base(name='base', state=3)
 35  assert comp.name == 'base'
 36  assert comp.group == 'Base'
 37  assert comp.part == 'Base'
 38  assert comp.nports == 2
 39  assert comp.ports == [[1, 2]]
 40  assert comp.state == 3
 41  assert comp.sign == -1.0
 42  assert comp.e == module_under_test.flsme.CTE_E_RVS.to(module_under_test.u.um)
 43  assert isinstance(comp.medium, module_under_test.flsme.Medium)
 44
 45def test_comp_base_medium_and_roughness_setters() -> None:
 46  comp = module_under_test.Comp_Base()
 47  medium = module_under_test.flsme.Medium(prd='water')
 48  comp.medium = medium
 49  assert comp.medium is medium
 50  comp.medium = 'water'
 51  assert isinstance(comp.medium, module_under_test.flsme.Medium)
 52  assert comp.medium.name == 'water'
 53  comp.e = 15
 54  roughness = comp.e
 55  assert isinstance(roughness, module_under_test.Quantity)
 56  assert getattr(roughness, 'magnitude') == 15
 57  assert getattr(roughness, 'units') == module_under_test.u.um
 58
 59def test_comp_base_medium_setter_rejects_invalid_type() -> None:
 60  comp = module_under_test.Comp_Base()
 61  with pytest.raises(TypeError, match='Medium must be a string or an instance of Medium'):
 62    comp.medium = 123  # type: ignore[assignment]
 63
 64def test_comp_base_state_setter_updates_state() -> None:
 65  comp = module_under_test.Comp_Base()
 66  comp.state = 2.5
 67  assert comp.state == 2.5
 68
 69def test_comp_base_calc_methods_and_clone() -> None:
 70  comp = module_under_test.Comp_Base(name='base')
 71
 72  assert comp.calcH(5).to(module_under_test.u.m).magnitude == 0.0
 73  assert comp.calcP(5).to(module_under_test.u.bar).magnitude == pytest.approx(0.0)
 74
 75  clone = comp.clone()
 76  assert isinstance(clone, module_under_test.Comp_Base)
 77  assert clone is not comp
 78  assert clone.name == comp.name
 79  assert clone.medium is not comp.medium
 80  assert clone.medium.name == comp.medium.name
 81
 82def test_comp_base_calcq_uses_root_result(monkeypatch) -> None:
 83  comp = module_under_test.Comp_Base()
 84
 85  def fake_root(func=None, x0=None, method=None):
 86    assert x0 == 200
 87    assert method in ('hybr', 'lm', 'df-sane')
 88    assert func(12) == pytest.approx(0.0)
 89    return types.SimpleNamespace(success=True, x=[12], message='ok', status=1)
 90
 91  def fake_calcH(flow, sense=1, pin=1, pout=2):
 92    assert sense == 1
 93    assert pin == 1
 94    assert pout == 2
 95    return flow.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude * module_under_test.u.m / 12
 96
 97  monkeypatch.setattr(module_under_test, 'root', fake_root)
 98  monkeypatch.setattr(comp, 'calcH', fake_calcH)
 99
100  result = comp.calcQ(1 * module_under_test.u.m, guess=200)
101  assert result.magnitude == 12
102  assert result.units == module_under_test.u.m**3 / module_under_test.u.h
103
104def test_comp_base_string_representation_contains_metadata() -> None:
105  comp = module_under_test.Comp_Base(name='base', state=2)
106
107  text = str(comp)
108  assert 'Component: "base" [Base]' == text
109
110def test_comp_base_format_representation_supports_detail() -> None:
111  comp = module_under_test.Comp_Base(name='base', state=2)
112
113  text = f'{comp:1}'
114  assert 'Component: "base" [Base:Base]' in text
115  assert 'state: 2' in text
116  assert 'sign: -1' in text
117
118def test_comp_dummy_initializes_as_resistance() -> None:
119  comp = module_under_test.Comp_Dummy(name='dummy')
120
121  assert comp.group == 'Base'
122  assert comp.part == 'Dummy'
123  assert comp.name == 'dummy'
124  assert isinstance(comp.medium, module_under_test.flsme.Medium)
125
126def test_comp_reverse_delegates_calc_methods_and_attributes() -> None:
127  class ReverseableComp(module_under_test.Comp_Base):
128    def __init__(self):
129      super().__init__(name='wrapped')
130
131    def calcK(self, Q, sense=1, pin=1, pout=2):
132      return (Q, sense, pin, pout)
133
134    def calcH(self, Q, sense=1, pin=1, pout=2):
135      return sense * 3 * module_under_test.u.m
136
137  wrapped = ReverseableComp()
138  comp = module_under_test.Comp_Reverse(name='rev', reverse=wrapped)
139
140  assert comp.calcK(5, sense=1, pin=2, pout=3) == (5, -1, 2, 3)
141  assert comp.calcH(5, sense=1, pin=2, pout=3) == -3 * module_under_test.u.m
142  assert comp.name == 'rev'
143  assert comp.part == 'Reverse'
144  assert 'reverse:' in str(comp)
145  assert '"wrapped"  [Base]' in str(comp)
146  assert ' reverse' in f'{comp:1}'
147  assert 'Component: "wrapped" [Base:Base]' in f'{comp:1}'
148
149def test_comp_reverse_raises_when_wrapped_component_has_no_calck() -> None:
150  class HeadOnlyComp(module_under_test.Comp_Base):
151    def calcH(self, Q, sense=1, pin=1, pout=2):
152      return 0 * module_under_test.u.m
153
154  wrapped = HeadOnlyComp(name='wrapped')
155  comp = module_under_test.Comp_Reverse(reverse=wrapped)
156
157  with pytest.raises(AttributeError, match='has no calcK'):
158    comp.calcK(5)

Tests: test_comp_pump

  1'''Auto-generated tests for fluidsolve.comp_pump.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring
  5
  6import inspect
  7import pytest
  8import fluidsolve.comp_pump as module_under_test
  9
 10def test_module_importable() -> None:
 11  assert module_under_test is not None
 12
 13@pytest.mark.parametrize('name', ['Comp_Pump', 'Comp_PumpCentrifugal', 'Comp_PumpParallel', 'Comp_PumpSerial'])
 14def test_public_classes_exist(name: str) -> None:
 15  obj = getattr(module_under_test, name)
 16  assert inspect.isclass(obj)
 17
 18def test_public_functions_are_callable() -> None:
 19  public_function_names = [
 20    name
 21    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 22    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 23  ]
 24
 25  for name in public_function_names:
 26    obj = getattr(module_under_test, name)
 27    assert callable(obj)
 28
 29@pytest.mark.parametrize('name', ['N_CURVE_POINTS', 'Quantity', 'u'])
 30def test_public_variables_exist(name: str) -> None:
 31  assert hasattr(module_under_test, name)
 32
 33def test_comp_pump_requires_curve_data_by_default() -> None:
 34  with pytest.raises(ValueError, match='No pump data'):
 35    module_under_test.Comp_Pump(speed0=2900)
 36
 37def test_comp_pump_initializes_curve_properties_and_metadata() -> None:
 38  pump = module_under_test.Comp_Pump(
 39    vendor='demo',
 40    spec='model',
 41    din=50,
 42    dout=40,
 43    speed0=2900,
 44    dataQH=[0, 12, 10, 6, 20, 0],
 45  )
 46
 47  assert pump.vendor == 'demo'
 48  assert pump.spec == 'model'
 49  assert pump.din == 50 * module_under_test.u.mm
 50  assert pump.dout == 40 * module_under_test.u.mm
 51  assert pump.speed0 == 2900 * module_under_test.u.rpm
 52  assert pump.speed == 2900 * module_under_test.u.rpm
 53  assert pump.Qb == 0 * module_under_test.u.m**3 / module_under_test.u.h
 54  assert pump.Qe == 20 * module_under_test.u.m**3 / module_under_test.u.h
 55  assert pump.Qc == 0 * module_under_test.u.m**3 / module_under_test.u.h
 56  assert pump.Hb == 0 * module_under_test.u.m
 57  assert pump.He == 12 * module_under_test.u.m
 58
 59def test_comp_pump_calcH_and_calcQ_cover_forward_reverse_and_clamping() -> None:
 60  pump = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 12, 10, 6, 20, 0])
 61
 62  assert pump.calcH(5).to(module_under_test.u.m).magnitude == pytest.approx(9.0)
 63  assert pump.calcH(5, sense=-1).to(module_under_test.u.m).magnitude == 0.0
 64  assert pump.calcH(-5, sense=-1).to(module_under_test.u.m).magnitude == pytest.approx(9.0)
 65  assert pump.calcH(25).to(module_under_test.u.m).magnitude == 0.0
 66
 67  assert pump.calcQ(6 * module_under_test.u.m).to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(10.0)
 68  assert pump.calcQ(-1 * module_under_test.u.m).to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(21.666666666666668)
 69
 70def test_comp_pump_speed_setter_accepts_scalar_and_updates_quantity() -> None:
 71  pump = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 12, 10, 6, 20, 0])
 72
 73  pump.speed = 1450
 74  assert pump.speed == 1450 * module_under_test.u.rpm
 75  assert pump.calcH(5).to(module_under_test.u.m).magnitude == pytest.approx(9.0)
 76
 77def test_comp_pump_to_string_includes_metadata() -> None:
 78  pump = module_under_test.Comp_Pump(vendor='demo', spec='model', speed0=2900, dataQH=[0, 12, 10, 6, 20, 0])
 79
 80  text = pump.toString()
 81  assert 'Pump: demo: model' in text
 82  assert 'speed0:2900 revolutions_per_minute' in text
 83
 84def test_comp_pump_centrifugal_scales_curve_with_speed_and_impeller() -> None:
 85  pump = module_under_test.Comp_PumpCentrifugal(
 86    speed0=2900,
 87    speed=1450,
 88    impeller0=100,
 89    impeller=50,
 90    dataQH=[0, 12, 10, 6, 20, 0],
 91  )
 92
 93  assert pump.impeller0 == 100 * module_under_test.u.mm
 94  assert pump.impeller == 50 * module_under_test.u.mm
 95  assert pump.Qe.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(2.5)
 96  assert pump.calcH(2.5).to(module_under_test.u.m).magnitude == pytest.approx(0.375)
 97
 98def test_comp_pump_centrifugal_trims_negative_head_data() -> None:
 99  pump = module_under_test.Comp_PumpCentrifugal(
100    speed0=2900,
101    impeller0=100,
102    dataQH=[0, 12, 10, 4, 20, -1],
103  )
104
105  assert pump.Qe.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(10.0)
106  assert pump.calcH(20).to(module_under_test.u.m).magnitude == 0.0
107
108def test_comp_pump_serial_combines_pump_heads() -> None:
109  pump_a = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 10, 10, 5, 20, 0])
110  pump_b = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 8, 10, 4, 20, 0])
111  serial = module_under_test.Comp_PumpSerial(pumps=[pump_a, pump_b])
112
113  assert serial.Qb == 0 * module_under_test.u.m**3 / module_under_test.u.h
114  assert serial.Qe == 20 * module_under_test.u.m**3 / module_under_test.u.h
115  assert serial.calcH(10).to(module_under_test.u.m).magnitude == pytest.approx(9.0)
116  assert serial.calcH(10, sense=-1).to(module_under_test.u.m).magnitude == 0.0
117  assert serial.calcQ(9 * module_under_test.u.m).to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(10.0)
118
119def test_comp_pump_serial_requires_at_least_one_pump() -> None:
120  with pytest.raises(ValueError, match='less than min 1'):
121    module_under_test.Comp_PumpSerial(pumps=[])
122
123def test_comp_pump_parallel_combines_flows() -> None:
124  pump_a = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 10, 10, 5, 20, 0])
125  pump_b = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 8, 10, 4, 20, 0])
126  parallel = module_under_test.Comp_PumpParallel(pumps=[pump_a, pump_b])
127
128  assert parallel.Qb.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude > 0
129  assert parallel.Qe.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude > parallel.Qb.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
130  assert parallel.calcQ(5 * module_under_test.u.m).to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(17.5)
131  assert parallel.calcH(17.5).to(module_under_test.u.m).magnitude == pytest.approx(5.0)
132
133def test_serial_and_parallel_to_string_include_labels() -> None:
134  pump = module_under_test.Comp_Pump(speed0=2900, dataQH=[0, 10, 10, 5, 20, 0])
135
136  serial = module_under_test.Comp_PumpSerial(pumps=[pump])
137  parallel = module_under_test.Comp_PumpParallel(pumps=[pump])
138
139  assert 'Serial pumps:' in serial.toString()
140  assert 'Parallel pumps:' in parallel.toString()

Tests: test_comp_resist

  1'''Auto-generated tests for fluidsolve.comp_resist.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-class-docstring,missing-function-docstring
  5
  6import inspect
  7from types import SimpleNamespace
  8import pytest
  9import numpy as np
 10import fluidsolve.comp_resist as module_under_test
 11
 12def test_module_importable() -> None:
 13  assert module_under_test is not None
 14
 15@pytest.mark.parametrize('name', ['C_EntranceBeveled', 'Comp_Appendage', 'Comp_Bend', 'Comp_BendLong', 'Comp_ConicalReduction', 'Comp_Entrance', 'Comp_Hstatic',
 16                                  'Comp_PHE', 'Comp_Parallel', 'Comp_Parallel2', 'Comp_Serial', 'Comp_SharpReduction', 'Comp_Tube'])
 17def test_public_classes_exist(name: str) -> None:
 18  obj = getattr(module_under_test, name)
 19  assert inspect.isclass(obj)
 20
 21def test_public_functions_are_callable() -> None:
 22  public_function_names = [
 23    name
 24    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 25    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 26  ]
 27
 28  for name in public_function_names:
 29    obj = getattr(module_under_test, name)
 30    assert callable(obj)
 31
 32@pytest.mark.parametrize('name', ['Quantity', 'u'])
 33def test_public_variables_exist(name: str) -> None:
 34  assert hasattr(module_under_test, name)
 35
 36def test_comp_hstatic_properties_calcH_and_to_string() -> None:
 37  comp = module_under_test.Comp_Hstatic(name='hs', Hs_pos=5, Hs_neg=2)
 38
 39  assert comp.Hs == 3 * module_under_test.u.m
 40  assert comp.calcH(1, sense=1, pin=1, pout=2) == 3 * module_under_test.u.m
 41  assert comp.calcH(1, sense=1, pin=2, pout=1) == -3 * module_under_test.u.m
 42
 43  comp.Hs = 4
 44  assert comp.Hs == 4 * module_under_test.u.m
 45  assert 'Hs: 4.00 m' in comp.toString()
 46
 47def test_comp_appendage_calcH_uses_loss_coefficient_and_to_string() -> None:
 48  class FixedKAppendage(module_under_test.Comp_Appendage):
 49    def __init__(self) -> None:
 50      super().__init__(name='app', medium='water')
 51      self._D = 50 * module_under_test.u.mm
 52      self._L = 2 * module_under_test.u.m
 53
 54    def calcK(self, Q, sense=1, pin=1, pout=2):
 55      return 4.0
 56
 57  comp = FixedKAppendage()
 58  head = comp.calcH(10)
 59
 60  expected = module_under_test.flsu.KtoH(4.0, module_under_test.flsu.Qtov(10 * module_under_test.u.m**3 / module_under_test.u.h, comp._D)) * comp.sign  # pylint: disable=protected-access
 61  assert head.to(module_under_test.u.m).magnitude == pytest.approx(expected.to(module_under_test.u.m).magnitude)
 62  assert 'L: 2.00 m' in comp.toString()
 63  assert 'D: 50.00 mm' in comp.toString()
 64
 65def test_comp_tube_properties_and_calcH_with_static_head(monkeypatch) -> None:
 66  tube = module_under_test.Comp_Tube(L=10, D=50, Hs_pos=2)
 67
 68  monkeypatch.setattr(tube, 'calcK', lambda *args, **kwargs: 0.0)
 69  assert tube.calcH(0).to(module_under_test.u.m).magnitude == pytest.approx(2.0)
 70
 71  tube.L = 12
 72  tube.D = 60
 73  tube.Hs = 1
 74  assert tube.L == 12 * module_under_test.u.m
 75  assert tube.D == 60 * module_under_test.u.mm
 76  assert tube.Hs == 1 * module_under_test.u.m
 77
 78def test_comp_bend_and_bendlong_delegate_to_fluids_helpers(monkeypatch) -> None:
 79  monkeypatch.setattr(module_under_test.fu, 'Reynolds', lambda **kwargs: 123.0)
 80  monkeypatch.setattr(module_under_test.fu, 'friction_factor', lambda *args, **kwargs: 0.02)
 81  monkeypatch.setattr(module_under_test.fu, 'bend_rounded', lambda **kwargs: 7.5)
 82  monkeypatch.setattr(module_under_test.fu, 'bend_miter', lambda *args, **kwargs: 4.5 if 'L_unimpeded' not in kwargs else 5.5)
 83
 84  bend = module_under_test.Comp_Bend(D=80)
 85  assert bend.calcK(10, sense=1) == pytest.approx(7.5)
 86
 87  bend_long = module_under_test.Comp_BendLong(D=80)
 88  assert bend_long.calcK(10, sense=1) == pytest.approx(4.5)
 89
 90  bend_long_lu = module_under_test.Comp_BendLong(D=80, Lu=1)
 91  assert bend_long_lu.calcK(10, sense=1) == pytest.approx(5.5)
 92
 93def test_comp_entrance_selects_entrance_or_exit_loss(monkeypatch) -> None:
 94  monkeypatch.setattr(module_under_test.fu, 'entrance_sharp', lambda: 1.25)
 95  monkeypatch.setattr(module_under_test.fu, 'exit_normal', lambda: 0.75)
 96
 97  comp = module_under_test.Comp_Entrance(D=50)
 98  assert comp.calcK(10, sense=1, pin=1, pout=2) == pytest.approx(1.25)
 99  assert comp.calcK(10, sense=1, pin=2, pout=1) == pytest.approx(0.75)
100
101def test_sharp_and_conical_reduction_validate_and_switch_branches(monkeypatch) -> None:
102  with pytest.raises(ValueError, match='must be larger than D2'):
103    module_under_test.Comp_SharpReduction(D1=40, D2=50)
104
105  monkeypatch.setattr(module_under_test.fu, 'contraction_sharp', lambda **kwargs: 2.0)
106  monkeypatch.setattr(module_under_test.fu, 'diffuser_sharp', lambda **kwargs: 3.0)
107  sharp = module_under_test.Comp_SharpReduction(D1=80, D2=40)
108  assert sharp.calcK(10, sense=1, pin=1, pout=2) == pytest.approx(2.0)
109  assert sharp.calcK(10, sense=1, pin=2, pout=1) == pytest.approx(3.0)
110
111  with pytest.raises(ValueError, match='must be larger than D2'):
112    module_under_test.Comp_ConicalReduction(D1=40, D2=50, L=1)
113
114  monkeypatch.setattr(module_under_test.fu, 'Reynolds', lambda **kwargs: 100.0)
115  monkeypatch.setattr(module_under_test.fu, 'friction_factor', lambda *args, **kwargs: 0.02)
116  monkeypatch.setattr(module_under_test.fu, 'fittings', SimpleNamespace(
117    contraction_conical=lambda **kwargs: 4.0,
118    diffuser_conical=lambda **kwargs: 5.0,
119  ), raising=False)
120  conical = module_under_test.Comp_ConicalReduction(D1=80, D2=40, L=1)
121  assert conical.calcK(10, sense=1, pin=1, pout=2) == pytest.approx(4.0)
122  assert conical.calcK(10, sense=1, pin=2, pout=1) == pytest.approx(5.0)
123
124def test_beveled_entrance_forward_and_reverse_flow(monkeypatch) -> None:
125  monkeypatch.setattr(module_under_test.fu, 'entrance_beveled', lambda *args, **kwargs: 6.0)
126  with pytest.raises(TypeError, match='argument left'):
127    module_under_test.C_EntranceBeveled(D=50, Lb=5, R=45)
128
129def test_comp_serial_combines_components_and_builds_profile() -> None:
130  class FixedHead(module_under_test.flsb.Comp_Base):
131    def __init__(self, name: str, head: float) -> None:
132      super().__init__(name=name)
133      self._head = head * module_under_test.u.m
134
135    def calcH(self, Q, sense=1, pin=1, pout=2):
136      return self._head * sense
137
138  comp_a = FixedHead('a', 2)
139  comp_b = FixedHead('b', 3)
140  serial = module_under_test.Comp_Serial(comps=[comp_a, comp_b])
141
142  assert serial.components == [comp_a, comp_b]
143  assert serial.getComp(0) is comp_a
144  assert serial.calcH(1).to(module_under_test.u.m).magnitude == pytest.approx(5.0)
145
146  profile = serial.calcHprofile(1, sense=1, incr=True)
147  assert [point.name for point in profile] == ['0:a', '1:b', 'Tot']
148  assert profile[0].H.to(module_under_test.u.m).magnitude == pytest.approx(2.0)
149  assert profile[1].H.to(module_under_test.u.m).magnitude == pytest.approx(5.0)
150
151  comp_c = FixedHead('c', 4)
152  assert serial.setComp(1, comp_c) is comp_c
153  assert serial.addComp(comp_b) is comp_b
154  assert 'Sub-Components: (3):' in serial.toString()
155  assert 'idx | Comp' in serial.toString()
156  assert 'a' in serial.toString()
157
158def test_comp_parallel_validates_guess_and_uses_root_result(monkeypatch) -> None:
159  class FixedHead(module_under_test.flsb.Comp_Base):
160    def __init__(self, name: str, head: float) -> None:
161      super().__init__(name=name)
162      self._head = head * module_under_test.u.m
163
164    def calcH(self, Q, sense=1, pin=1, pout=2):
165      return self._head
166
167  comp_a = FixedHead('a', 2)
168  comp_b = FixedHead('b', 2)
169  parallel = module_under_test.Comp_Parallel(comps=[comp_a, comp_b], guess=[1.0])
170
171  parallel.guess = [1.0, 1.0]
172
173  def fake_root(func=None, x0=None, args=None, method=None):
174    assert callable(func)
175    assert list(x0) == [1.0, 1.0]
176    assert args == pytest.approx((2.0,))
177    assert method in ('hybr', 'lm', 'df-sane')
178    return SimpleNamespace(success=True, x=[0.5, 1.5], message='ok', status=1)
179
180  monkeypatch.setattr(module_under_test, 'root', fake_root)
181  head = parallel.calcH(2)
182  assert head.to(module_under_test.u.m).magnitude == pytest.approx(2.0)
183  assert list(parallel.getQ().magnitude) == pytest.approx([0.5, 1.5])
184  assert 'Sub-Components: (2):' in parallel.toString()
185  assert 'idx | Comp' in parallel.toString()
186  assert parallel.components == [comp_a, comp_b]
187
188def test_comp_parallel2_uses_solver_result_and_reports_failure(monkeypatch) -> None:
189  class FixedHead(module_under_test.flsb.Comp_Base):
190    def __init__(self, name: str, head: float) -> None:
191      super().__init__(name=name)
192      self._head = head * module_under_test.u.m
193
194    def calcH(self, Q, sense=1, pin=1, pout=2):
195      return self._head
196
197  comp_a = FixedHead('a', 2)
198  comp_b = FixedHead('b', 2)
199  parallel = module_under_test.Comp_Parallel2(comps=[comp_a, comp_b], guess=1.0)
200
201  def fake_ok(func=None, x0=None, args=None, method=None):
202    assert callable(func)
203    assert x0 == 1.0
204    assert args == pytest.approx((2.0,))
205    assert method in ('hybr', 'lm', 'df-sane')
206    return SimpleNamespace(success=True, x=np.array([0.75]), message='ok', status=1)
207
208  monkeypatch.setattr(module_under_test, 'root', fake_ok)
209  head = parallel.calcH(2)
210  assert head.to(module_under_test.u.m).magnitude == pytest.approx(2.0)
211  assert parallel.getQ()[0].magnitude == pytest.approx(0.75)
212  assert parallel.components == [comp_a, comp_b]
213
214  def fake_fail(func=None, x0=None, args=None, method=None):
215    assert callable(func)
216    assert x0 == 1.0
217    assert args == pytest.approx((2.0,))
218    assert method in ('hybr', 'lm', 'df-sane')
219    return SimpleNamespace(success=False, x=np.array([0.75]), message='failed', status=4)
220
221  monkeypatch.setattr(module_under_test, 'root', fake_fail)
222  with pytest.warns(RuntimeWarning, match='failed|no convergence'):
223    parallel.calcH(2)
224  assert parallel.getQ()[0].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(2.0)
225  assert parallel.getQ()[1].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(0.0)
226  assert 'Sub-Components: (2):' in parallel.toString()
227  assert 'idx | Comp' in parallel.toString()

Tests: test_comp_valve

  1'''Auto-generated tests for fluidsolve.comp_valve.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring
  5
  6import inspect
  7import pytest
  8import fluidsolve.comp_valve as module_under_test
  9
 10def test_module_importable() -> None:
 11  assert module_under_test is not None
 12
 13@pytest.mark.parametrize('name', ['Comp_Valve', 'Comp_Valve_01', 'Comp_Valve_3W', 'Comp_Valve_DS', 'Comp_Valve_Kv', 'Comp_Valve_NR'])
 14def test_public_classes_exist(name: str) -> None:
 15  obj = getattr(module_under_test, name)
 16  assert inspect.isclass(obj)
 17
 18def test_public_functions_are_callable() -> None:
 19  public_function_names = [
 20    name
 21    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 22    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 23  ]
 24
 25  for name in public_function_names:
 26    obj = getattr(module_under_test, name)
 27    assert callable(obj)
 28
 29@pytest.mark.parametrize('name', ['Quantity', 'u'])
 30def test_public_variables_exist(name: str) -> None:
 31  assert hasattr(module_under_test, name)
 32
 33def test_comp_valve_base_properties_and_to_string() -> None:
 34  valve = module_under_test.Comp_Valve(name='valve', D=50, state=1)
 35
 36  assert valve.group == 'Resistance'
 37  assert valve.part == 'Valve'
 38  assert valve.state == 1
 39  assert valve.calcK(1, sense=1) == 0.0
 40  assert 'State:1.00' in valve.toString()
 41  assert 'D:50.00 mm' in valve.toString()
 42
 43def test_comp_valve_base_calcH_uses_loss_conversion(monkeypatch) -> None:
 44  valve = module_under_test.Comp_Valve(D=50)
 45
 46  monkeypatch.setattr(module_under_test.flsu, 'Qtov', lambda flow, diameter: 3.0)
 47  monkeypatch.setattr(module_under_test.flsu, 'KtoH', lambda loss, velocity: (loss + velocity) * module_under_test.u.m)
 48  monkeypatch.setattr(valve, 'calcK', lambda *args, **kwargs: 2.0)
 49
 50  assert valve.calcH(10).magnitude == pytest.approx(-5.0)
 51
 52def test_comp_valve_nr_allows_only_forward_flow() -> None:
 53  valve = module_under_test.Comp_Valve_NR(D=50)
 54
 55  assert valve.calcK(1, sense=1, pin=1, pout=2) == 0.0
 56  assert valve.calcK(1, sense=1, pin=2, pout=1) == 1e6
 57  assert valve.calcK(1, sense=-1, pin=1, pout=2) == 1e6
 58
 59def test_comp_valve_01_switches_between_open_and_closed() -> None:
 60  valve = module_under_test.Comp_Valve_01(D=50, state=0)
 61  assert valve.calcK(1, sense=1) == 1e6
 62
 63  valve.state = 1
 64  assert valve.calcK(1, sense=1) == 0.0
 65
 66def test_comp_valve_kv_properties_setters_and_calcK() -> None:
 67  valve = module_under_test.Comp_Valve_Kv(D=50, Kvs=25, R=4, state=0.5)
 68
 69  assert valve.Kvs == 25
 70  assert valve.R == 4
 71  assert valve.connections() == [(1, 2)]
 72  assert valve.connections(state=0.0) == [(1, 2)]
 73  assert valve.connections(state=0.73) == [(1, 2)]
 74  expected_kv_half = 25.0 * (4.0 ** 0.5 - 1.0) / 3.0
 75  assert valve.calcK(1, sense=1) == pytest.approx(module_under_test.flsu.KvtoK(expected_kv_half, 50 * module_under_test.u.mm))
 76
 77  valve.Kvs = 30
 78  valve.R = 9
 79  valve.state = 1.0
 80  assert valve.Kvs == 30
 81  assert valve.R == 9
 82  assert valve.calcK(1, sense=1) == pytest.approx(module_under_test.flsu.KvtoK(30.0, 50 * module_under_test.u.mm))
 83
 84  valve.state = 0.0
 85  assert valve.calcK(1, sense=1) == 1e6
 86
 87  valve.state = 0.01
 88  k_001 = valve.calcK(1, sense=1)
 89  valve.state = 0.02
 90  k_002 = valve.calcK(1, sense=1)
 91
 92  assert k_001 <= 1e6
 93  assert k_001 > k_002
 94
 95def test_comp_valve_3w_connections_and_calcK_errors() -> None:
 96  valve = module_under_test.Comp_Valve_3W(D=50, state=1)
 97
 98  assert valve.connections() == [(1, 2)]
 99  assert valve.connections(state=2) == [(1, 3)]
100  assert valve.calcK(1, pin=1, pout=2) == pytest.approx(0.7)
101  assert valve.calcK(1, pin=2, pout=1) == pytest.approx(0.7)
102  assert valve.calcK(1, pin=2, pout=3) == 1e6
103
104  valve.state = 3
105  with pytest.raises(ValueError, match='Invalid state for 3-way valve'):
106    valve.calcK(1, pin=1, pout=2)
107
108  valve.state = 1
109  with pytest.raises(ValueError, match='Invalid ports for 3-way valve'):
110    valve.calcK(1, pin=0, pout=2)
111  with pytest.raises(ValueError, match='pin and pout must be different'):
112    valve.calcK(1, pin=1, pout=1)
113
114def test_comp_valve_ds_connections_and_calcK_errors() -> None:
115  valve = module_under_test.Comp_Valve_DS(D=50, state=1)
116
117  assert valve.connections() == [(1, 2), (3, 4)]
118  assert valve.connections(state=1) == [(1, 2), (3, 4)]
119  assert valve.connections(state=2) == [(1, 2), (1, 3), (3, 4)]
120  assert valve.calcK(1, pin=1, pout=2) == pytest.approx(0.7)
121  assert valve.calcK(1, pin=2, pout=4) == 1e6
122
123  valve.state = 2
124  assert valve.calcK(1, pin=1, pout=3) == pytest.approx(0.7)
125  assert valve.calcK(1, pin=2, pout=4) == pytest.approx(0.7)
126
127  valve.state = 9
128  with pytest.raises(ValueError, match='Invalid state for 3-way valve'):
129    valve.calcK(1, pin=1, pout=2)
130
131  valve.state = 1
132  with pytest.raises(ValueError, match='Invalid ports for 3-way valve'):
133    valve.calcK(1, pin=5, pout=2)
134  with pytest.raises(ValueError, match='pin and pout must be different'):
135    valve.calcK(1, pin=1, pout=1)

Tests: test_core

  1'''Behavioral unit tests for fluidsolve.core.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,protected-access
  5
  6import inspect
  7import pytest
  8import fluidsolve.core as module_under_test
  9
 10def test_module_importable() -> None:
 11  assert module_under_test is not None
 12
 13def test_public_classes_exist() -> None:
 14  public_class_names = [
 15    name
 16    for name, obj in inspect.getmembers(module_under_test, inspect.isclass)
 17    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 18  ]
 19
 20  for name in public_class_names:
 21    obj = getattr(module_under_test, name)
 22    assert inspect.isclass(obj)
 23
 24@pytest.mark.parametrize(
 25  'name',
 26  [
 27    'getComp',
 28    'getDefaultMaterial',
 29    'getDefaultMedium',
 30    'getNetwork',
 31    'getPath',
 32    'getWpt',
 33    'initFluidsolve',
 34    'registerAllComps',
 35    'registerComp',
 36    'registerComps',
 37    'setDefaultMaterial',
 38    'setDefaultMedium',
 39  ],
 40)
 41def test_public_functions_are_callable(name: str) -> None:
 42  obj = getattr(module_under_test, name)
 43  assert callable(obj)
 44
 45@pytest.mark.parametrize('name', ['Quantity', 'u'])
 46def test_public_variables_exist(name: str) -> None:
 47  assert hasattr(module_under_test, name)
 48
 49def test_init_fluidsolve_applies_prefixes_and_default_objects() -> None:
 50  material = module_under_test.flsma.Material(name='steel', rho=800.0, k=2.0, e=5.0)
 51  medium = module_under_test.flsme.Medium(prd='water')
 52
 53  module_under_test.initFluidsolve(
 54    prefix_comp='X',
 55    prefix_wpt='Y',
 56    default_material=material,
 57    default_medium=medium,
 58  )
 59
 60  assert module_under_test._prefix_comp == 'X'
 61  assert module_under_test._prefix_wpt == 'Y'
 62  assert module_under_test.getDefaultMaterial() is material
 63  assert module_under_test.getDefaultMedium() is medium
 64
 65def test_init_fluidsolve_converts_default_names_to_objects() -> None:
 66  module_under_test.initFluidsolve(default_material='rvs', default_medium='water')
 67
 68  assert isinstance(module_under_test.getDefaultMaterial(), module_under_test.flsma.Material)
 69  assert isinstance(module_under_test.getDefaultMedium(), module_under_test.flsme.Medium)
 70
 71def test_register_comp_rejects_duplicate_by_default(monkeypatch) -> None:
 72  monkeypatch.setattr(module_under_test, '_comps', {'Demo': object()})
 73
 74  with pytest.raises(ValueError, match='already registered'):
 75    module_under_test.registerComp('Demo', object())
 76
 77def test_register_comp_returns_false_when_duplicate_and_raiseerror_false(monkeypatch) -> None:
 78  monkeypatch.setattr(module_under_test, '_comps', {'Demo': object()})
 79
 80  assert module_under_test.registerComp('Demo', object(), raiseerror=False) is False
 81
 82def test_register_comp_adds_new_entry(monkeypatch) -> None:
 83  registry = {}
 84  monkeypatch.setattr(module_under_test, '_comps', registry)
 85  marker = object()
 86
 87  assert module_under_test.registerComp('Demo', marker) is True
 88  assert registry['Demo'] is marker
 89
 90def test_register_comps_returns_false_if_any_registration_fails(monkeypatch) -> None:
 91  calls = []
 92
 93  def fake_register(name: str, value: object, raiseerror: bool=True) -> bool:
 94    calls.append((name, value, raiseerror))
 95    return name != 'Bad'
 96
 97  monkeypatch.setattr(module_under_test, 'registerComp', fake_register)
 98
 99  result = module_under_test.registerComps({'Good': 1, 'Bad': 2}, raiseerror=False)
100
101  assert result is False
102  assert calls == [('Good', 1, False), ('Bad', 2, False)]
103
104def test_register_all_comps_returns_false_if_any_group_fails(monkeypatch) -> None:
105  results = iter([True, False, True, True])
106  calls = []
107
108  def fake_register_comps(comps: dict, raiseerror: bool=True) -> bool:
109    calls.append((set(comps), raiseerror))
110    return next(results)
111
112  monkeypatch.setattr(module_under_test, 'registerComps', fake_register_comps)
113
114  assert module_under_test.registerAllComps() is False
115  assert len(calls) == 4
116  assert {'Dummy', 'Reverse'} == calls[0][0]
117  assert 'Hstatic' in calls[1][0]
118  assert 'Pump' in calls[2][0]
119  assert 'Valve_NR' in calls[3][0]
120
121def test_default_setters_replace_defaults() -> None:
122  material = module_under_test.flsma.Material(name='custom', rho=900.0, k=3.0, e=6.0)
123  medium = module_under_test.flsme.Medium(prd='water')
124
125  module_under_test.setDefaultMaterial(material)
126  module_under_test.setDefaultMedium(medium)
127
128  assert module_under_test.getDefaultMaterial() is material
129  assert module_under_test.getDefaultMedium() is medium
130
131def test_get_comp_builds_instance_with_defaults(monkeypatch) -> None:
132  class DummyComp:
133    def __init__(self, **kwargs):
134      self.kwargs = kwargs
135
136  medium = module_under_test.flsme.Medium(prd='water')
137  material = module_under_test.flsma.Material(name='custom', rho=900.0, k=2.0, e=4.0)
138  monkeypatch.setattr(module_under_test, '_comps', {'Demo': DummyComp})
139  monkeypatch.setattr(module_under_test, '_default_medium', medium)
140  monkeypatch.setattr(module_under_test, '_default_material', material)
141  monkeypatch.setattr(module_under_test, '_comp_index', 0)
142  monkeypatch.setattr(module_under_test, '_prefix_comp', 'C')
143
144  comp = module_under_test.getComp(comp='Demo', answer=42)
145
146  assert isinstance(comp, DummyComp)
147  assert comp.kwargs['name'] == 'CA'
148  assert comp.kwargs['medium'] is medium
149  assert comp.kwargs['e'] == material.e
150  assert comp.kwargs['answer'] == 42
151
152def test_get_comp_uses_explicit_values_and_rejects_unknown_type(monkeypatch) -> None:
153  class DummyComp:
154    def __init__(self, **kwargs):
155      self.kwargs = kwargs
156
157  medium = module_under_test.flsme.Medium(prd='water')
158  monkeypatch.setattr(module_under_test, '_comps', {'Demo': DummyComp})
159
160  comp = module_under_test.getComp(comp='Demo', name='P1', medium=medium, e=3, state=1)
161
162  assert comp.kwargs == {'name': 'P1', 'medium': medium, 'e': 3, 'state': 1}
163
164  with pytest.raises(ValueError, match='Component type "Missing" is not defined'):
165    module_under_test.getComp(comp='Missing')
166
167def test_get_wpt_builds_instance_with_generated_or_explicit_name(monkeypatch) -> None:
168  class DummyWpt:
169    def __init__(self, **kwargs):
170      self.kwargs = kwargs
171
172  monkeypatch.setattr(module_under_test, '_wpts', {'x': DummyWpt})
173  monkeypatch.setattr(module_under_test, '_wpt_index', 0)
174  monkeypatch.setattr(module_under_test, '_prefix_wpt', 'Wp')
175
176  auto = module_under_test.getWpt(wpt='x', pressure=1)
177  named = module_under_test.getWpt(wpt='x', name='Node1', pressure=2)
178
179  assert auto.kwargs == {'name': 'Wp0', 'pressure': 1}
180  assert named.kwargs == {'name': 'Node1', 'pressure': 2}
181
182  with pytest.raises(ValueError, match='Workingpoint type "missing" is not defined'):
183    module_under_test.getWpt(wpt='missing')
184
185def test_get_path_and_network_delegate_to_target_classes(monkeypatch) -> None:
186  class DummyPath:
187    def __init__(self, **kwargs):
188      self.kwargs = kwargs
189
190  class DummyNetwork:
191    def __init__(self, **kwargs):
192      self.kwargs = kwargs
193
194  monkeypatch.setattr(module_under_test.flspath, 'Path', DummyPath)
195  monkeypatch.setattr(module_under_test.flsnet, 'Network', DummyNetwork)
196
197  path = module_under_test.getPath(name='P', comps=[1, 2])
198  network = module_under_test.getNetwork(name='N', components=[])
199  path_kwargs = getattr(path, 'kwargs')
200  network_kwargs = getattr(network, 'kwargs')
201
202  assert isinstance(path, DummyPath)
203  assert isinstance(network, DummyNetwork)
204  assert path_kwargs == {'name': 'P', 'comps': [1, 2]}
205  assert network_kwargs == {'name': 'N', 'components': []}
206
207def test_generated_names_follow_current_sequence_rules(monkeypatch) -> None:
208  monkeypatch.setattr(module_under_test, '_comp_index', 0)
209  monkeypatch.setattr(module_under_test, '_wpt_index', 0)
210  monkeypatch.setattr(module_under_test, '_prefix_comp', 'C')
211  monkeypatch.setattr(module_under_test, '_prefix_wpt', 'Wp')
212
213  comp_names = [module_under_test._getCompName() for _ in range(28)]
214  wpt_names = [module_under_test._getWptName() for _ in range(3)]
215
216  assert comp_names[:4] == ['CA', 'CB', 'CC', 'CD']
217  assert comp_names[25:] == ['CZ', 'CAA', 'CAB']
218  assert wpt_names == ['Wp0', 'Wp1', 'Wp2']

Tests: test_material

  1'''Auto-generated tests for fluidsolve.material.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring
  5
  6import inspect
  7import pytest
  8import fluidsolve.material as module_under_test
  9
 10def test_module_importable() -> None:
 11  assert module_under_test is not None
 12
 13@pytest.mark.parametrize('name', ['Material'])
 14def test_public_classes_exist(name: str) -> None:
 15  obj = getattr(module_under_test, name)
 16  assert inspect.isclass(obj)
 17
 18def test_public_functions_are_callable() -> None:
 19  public_function_names = [
 20    name
 21    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 22    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 23  ]
 24
 25  for name in public_function_names:
 26    obj = getattr(module_under_test, name)
 27    assert callable(obj)
 28
 29@pytest.mark.parametrize('name', ['CTE_E_RVS', 'CTE_G', 'CTE_K', 'CTE_NT', 'CTE_RHO', 'Quantity', 'u'])
 30def test_public_variables_exist(name: str) -> None:
 31  assert hasattr(module_under_test, name)
 32
 33def test_material_defaults_and_public_properties() -> None:
 34  material = module_under_test.Material()
 35  temperature = material.T
 36
 37  assert material.name == 'mat'
 38  assert getattr(temperature, 'to')(module_under_test.u.degC).magnitude == pytest.approx(20.0)
 39  assert material.rho == module_under_test.CTE_RHO
 40  assert material.k == module_under_test.CTE_K
 41  assert material.e == module_under_test.CTE_E_RVS
 42  assert material.cmat is None
 43
 44def test_material_init_accepts_custom_values_and_units() -> None:
 45  material = module_under_test.Material(
 46    mat='steel',
 47    name='Steel',
 48    T=30 * module_under_test.u.degC,
 49    rho=800.0,
 50    k=2.0,
 51    e=5.0,
 52  )
 53  temperature = material.T
 54
 55  assert material.name == 'Steel'
 56  assert getattr(temperature, 'to')(module_under_test.u.degC).magnitude == pytest.approx(30.0)
 57  assert material.rho == 800 * module_under_test.u.kg / module_under_test.u.m**3
 58  assert material.k == 2 * module_under_test.u.W / module_under_test.u.m / module_under_test.u.degK
 59  assert material.e == 5 * module_under_test.u.um
 60
 61def test_material_setters_update_stored_values() -> None:
 62  material = module_under_test.Material()
 63
 64  material.name = 'custom'
 65  material.T = 40 * module_under_test.u.degC
 66  material.rho = 950.0
 67  material.k = 0.6
 68  material.e = 4.0
 69  temperature = material.T
 70
 71  assert material.name == 'custom'
 72  assert getattr(temperature, 'to')(module_under_test.u.degC).magnitude == pytest.approx(40.0)
 73  assert material.rho == 950 * module_under_test.u.kg / module_under_test.u.m**3
 74  assert material.k == 0.6 * module_under_test.u.W / module_under_test.u.m / module_under_test.u.degK
 75  assert material.e == 4 * module_under_test.u.um
 76
 77def test_material_to_string_and_repr_cover_detail_levels() -> None:
 78  material = module_under_test.Material(name='steel', rho=800.0, k=2.0, e=5.0)
 79
 80  text_basic = material.toString()
 81  text_detail = material.toString(1)
 82  text_formatted = f'{material:1}'
 83  rep = repr(material)
 84
 85  assert 'Material steel:' in text_basic
 86  assert 'rho:800.00' in text_basic
 87  assert 'kg/m' in text_basic
 88  assert 'T:' in text_detail
 89  assert 'k:' in text_detail
 90  assert text_formatted == text_detail
 91  assert 'Material(name="steel"' in rep
 92
 93def test_material_update_product_is_safe_without_library_backend() -> None:
 94  material = module_under_test.Material(mat='steel', rho=700.0, k=3.0, e=7.0)
 95
 96  material._updateProduct()  # pylint: disable=protected-access
 97
 98  assert material.cmat is None
 99  assert material.rho == 700 * module_under_test.u.kg / module_under_test.u.m**3
100  assert material.k == 3 * module_under_test.u.W / module_under_test.u.m / module_under_test.u.degK
101  assert material.e == 7 * module_under_test.u.um

Tests: test_medium

  1'''Behavioral unit tests for fluidsolve.medium.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring
  5
  6import inspect
  7import pytest
  8import fluidsolve.medium as module_under_test
  9
 10u = module_under_test.unitRegistry
 11
 12def test_module_importable() -> None:
 13  assert module_under_test is not None
 14
 15@pytest.mark.parametrize('name', ['Medium'])
 16def test_public_classes_exist(name: str) -> None:
 17  obj = getattr(module_under_test, name)
 18  assert inspect.isclass(obj)
 19
 20def test_public_functions_are_callable() -> None:
 21  public_function_names = [
 22    name
 23    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 24    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 25  ]
 26
 27  for name in public_function_names:
 28    obj = getattr(module_under_test, name)
 29    assert callable(obj)
 30
 31@pytest.mark.parametrize(
 32  'name',
 33  ['CTE_E_RVS', 'CTE_G', 'CTE_K', 'CTE_MU', 'CTE_NP', 'CTE_NT', 'CTE_NU', 'CTE_RHO', 'CTE_WATER', 'Quantity', 'u', 'unitRegistry'],
 34)
 35def test_public_variables_exist(name: str) -> None:
 36  assert hasattr(module_under_test, name)
 37
 38def test_medium_defaults_are_initialized_from_reference_water() -> None:
 39  medium = module_under_test.Medium()
 40  temperature = medium.T
 41  pressure = medium.p
 42  density = medium.rho
 43  viscosity = medium.mu
 44  conductivity = medium.k
 45  assert medium.name == 'water'
 46  assert medium.cprd is not None
 47  assert getattr(temperature, 'to')(u.degC).magnitude == pytest.approx(20.0, rel=1e-3)
 48  assert getattr(pressure, 'to')(u.bar).magnitude == pytest.approx((1.0 * u.atm).to(u.bar).magnitude, rel=1e-3)
 49  assert getattr(density, 'magnitude') > 0.0
 50  assert getattr(viscosity, 'magnitude') > 0.0
 51  assert getattr(conductivity, 'magnitude') > 0.0
 52
 53def test_medium_accepts_custom_conditions_and_name() -> None:
 54  medium = module_under_test.Medium(name='hot_water', T=50.0 * u.degC, p=2.0 * u.bar)
 55  temperature = medium.T
 56  pressure = medium.p
 57
 58  assert medium.name == 'hot_water'
 59  assert getattr(temperature, 'to')(u.degC).magnitude == pytest.approx(50.0, rel=1e-3)
 60  assert getattr(pressure, 'to')(u.bar).magnitude == pytest.approx(2.0, rel=1e-3)
 61
 62def test_medium_manual_properties_allowed_without_thermo_product() -> None:
 63  medium = module_under_test.Medium(
 64    prd='',
 65    name='manual',
 66    rho=950.0,
 67    mu=0.0012,
 68    k=0.55,
 69  )
 70  density = medium.rho
 71  viscosity = medium.mu
 72  conductivity = medium.k
 73
 74  assert medium.cprd is None
 75  assert medium.name == 'manual'
 76  assert getattr(density, 'to')(u.kg / u.m**3).magnitude == pytest.approx(950.0)
 77  assert getattr(viscosity, 'to')(u.Pa * u.s).magnitude == pytest.approx(0.0012)
 78  assert getattr(conductivity, 'to')(u.W / u.m / u.degK).magnitude == pytest.approx(0.55)
 79
 80def test_medium_without_product_requires_rho_mu_and_k() -> None:
 81  with pytest.raises(ValueError, match='Medium must have a valid prd or have a rho, mu and k'):
 82    module_under_test.Medium(prd='')
 83
 84def test_medium_property_setters_update_values_and_keep_overrides() -> None:
 85  medium = module_under_test.Medium()
 86
 87  medium.rho = 975.0
 88  medium.mu = 0.00105
 89  medium.k = 0.62
 90  medium.T = 60.0 * u.degC
 91  medium.p = 3.0 * u.bar
 92  temperature = medium.T
 93  pressure = medium.p
 94  density = medium.rho
 95  viscosity = medium.mu
 96  conductivity = medium.k
 97
 98  assert getattr(temperature, 'to')(u.degC).magnitude == pytest.approx(60.0, rel=1e-3)
 99  assert getattr(pressure, 'to')(u.bar).magnitude == pytest.approx(3.0, rel=1e-3)
100  assert getattr(density, 'to')(u.kg / u.m**3).magnitude == pytest.approx(975.0, rel=1e-3)
101  assert getattr(viscosity, 'to')(u.Pa * u.s).magnitude == pytest.approx(0.00105, rel=1e-4)
102  assert getattr(conductivity, 'to')(u.W / u.m / u.degK).magnitude == pytest.approx(0.62, rel=1e-3)
103
104def test_medium_name_setter_and_text_representations() -> None:
105  medium = module_under_test.Medium(name='old_name')
106  medium.name = 'new_name'
107
108  text_basic = str(medium)
109  text_detail = medium.toString(detail=1)
110  text_formatted = f'{medium:1}'
111  rep = repr(medium)
112
113  assert medium.name == 'new_name'
114  assert 'Medium new_name:' in text_basic
115  assert 'rho:' in text_basic
116  assert 'mu:' in text_basic
117  assert 'T:' in text_detail
118  assert 'p:' in text_detail
119  assert 'k:' in text_detail
120  assert text_formatted == text_detail
121  assert 'Medium(name="new_name", prd="water"' in rep
122
123def test_medium_repr_for_empty_name_uses_default_signature() -> None:
124  medium = module_under_test.Medium(name='')
125
126  assert repr(medium).startswith('Medium(prd="water"')

Tests: test_network

  1'''Behavioral unit tests for fluidsolve.network.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,protected-access
  5
  6import inspect
  7import types
  8import numpy as np
  9import pytest
 10import fluidsolve.network as module_under_test
 11import fluidsolve.comp_valve as comp_valve
 12
 13def test_module_importable() -> None:
 14  assert module_under_test is not None
 15
 16@pytest.mark.parametrize('name', ['Network'])
 17def test_public_classes_exist(name: str) -> None:
 18  obj = getattr(module_under_test, name)
 19  assert inspect.isclass(obj)
 20
 21def test_public_functions_are_callable() -> None:
 22  public_function_names = [
 23    name
 24    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 25    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 26  ]
 27
 28  for name in public_function_names:
 29    obj = getattr(module_under_test, name)
 30    assert callable(obj)
 31
 32@pytest.mark.parametrize('name', ['Quantity', 'u'])
 33def test_public_variables_exist(name: str) -> None:
 34  assert hasattr(module_under_test, name)
 35
 36class DummyResist(module_under_test.flsb.Comp_Base):
 37  _group = 'Resistance'
 38  _part = 'DummyResist'
 39  _sign = -1.0
 40
 41  def __init__(self, name: str, head_factor: float=1.0) -> None:
 42    super().__init__(name=name)
 43    self.head_factor = head_factor
 44    self.calls = []
 45
 46  def calcH(self, Q, sense: int=1, pin: int=1, pout: int=2):
 47    self.calls.append((Q, sense, pin, pout))
 48    flow = Q.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude if hasattr(Q, 'to') else float(Q)
 49    # Dummy friction model: always dissipative head contribution.
 50    return -self.head_factor * abs(flow) * module_under_test.u.m
 51
 52class DummyPump(DummyResist):
 53  _group = 'Pump'
 54  _part = 'DummyPump'
 55  _sign = 1.0
 56
 57class DummyDirectionalResist(module_under_test.flsb.Comp_Base):
 58  _group = 'Resistance'
 59  _part = 'DummyDirectionalResist'
 60  _sign = -1.0
 61
 62  def __init__(self, name: str, k_forward: float=2.0, k_reverse: float=7.0) -> None:
 63    super().__init__(name=name)
 64    self.k_forward = k_forward
 65    self.k_reverse = k_reverse
 66    self.calls = []
 67
 68  def calcH(self, Q, sense: int=1, pin: int=1, pout: int=2):
 69    self.calls.append((Q, sense, pin, pout))
 70    flow = Q.to(module_under_test.u.m**3 / module_under_test.u.h).magnitude if hasattr(Q, 'to') else float(Q)
 71    k = self.k_forward if sense > 0 else self.k_reverse
 72    return -k * abs(flow) * module_under_test.u.m
 73
 74def _triangle_network() -> module_under_test.Network:
 75  return module_under_test.Network(
 76    name='Loop',
 77    components=[
 78      {'comp': DummyPump('P1', head_factor=4.0), 'nodes': ['A', 'B']},
 79      {'comp': DummyResist('R1', head_factor=1.5), 'nodes': ['B', 'C']},
 80      {'comp': DummyResist('R2', head_factor=2.0), 'nodes': ['C', 'A']},
 81    ],
 82  )
 83
 84def test_network_defaults_and_empty_strings() -> None:
 85  network = module_under_test.Network(name='N1')
 86
 87  assert not network.components
 88  assert not network.nodes
 89  assert not network.edges
 90  assert not network.segments
 91  assert not network.result
 92  assert network.nodeString() == ' Nodes (0):\n  ---\n'
 93  assert network.segmentsString() == ' Segments (0):\n  ---\n'
 94  assert network.resultString() == ' Result:\n   not yet calculated\n'
 95  assert network.functionString() == ' Functions: Combined incidence matrix (B:0) (C:0):\n  No segments in network\n'
 96
 97def test_network_add_components_validates_entries() -> None:
 98  comp = DummyResist('R1')
 99
100  with pytest.raises(ValueError, match='Invalid component entry'):
101    module_under_test.Network(components=['bad'])
102  with pytest.raises(ValueError, match='must contain "comp" and "nodes"'):
103    module_under_test.Network(components=[{'comp': comp}])
104  with pytest.raises(ValueError, match='Node count 1 does not match component ports 2'):
105    module_under_test.Network(components=[{'comp': comp, 'nodes': ['A']}])
106  with pytest.raises(ValueError, match=r'sense must be \+1 or -1'):
107    module_under_test.Network(components=[{'comp': comp, 'nodes': ['A', 'B'], 'sense': 0}])
108  with pytest.raises(AttributeError):
109    module_under_test.Network(components=[{'comp': object(), 'nodes': ['A', 'B']}])
110  with pytest.raises(ValueError, match='Invalid nodes'):
111    module_under_test.Network(components=[{'comp': comp, 'nodes': 'AB'}])
112
113def test_network_builds_segments_adjacency_tree_and_cycle_matrices() -> None:
114  network = _triangle_network()
115
116  assert len(network.components) == 3
117  assert [item['comp'].name for item in network.components] == ['P1', 'R1', 'R2']
118  assert list(network.nodes) == ['A', 'B', 'C']
119  assert len(network.segments) == 3
120  assert set(network.edges) == {'P1:A-B', 'R1:B-C', 'R2:C-A'}
121  assert network.adjacency['A'][0] == ('P1:A-B', 'B', 1)
122  assert ('R2:C-A', 'C', -1) in network.adjacency['A']
123  assert len(network.spanningTree) == 2
124  assert len(network.cycleBase) == 1
125  assert network.funcs['B'].shape == (3, 3)
126  assert network.funcs['C'].shape == (1, 3)
127  assert sorted(abs(value) for value in network.funcs['C'][0]) == [1.0, 1.0, 1.0]
128
129def test_network_cycle_basis_keeps_full_chord_path_loops() -> None:
130  network = module_under_test.Network(
131    name='QuadLoop',
132    components=[
133      {'comp': DummyPump('P1', head_factor=5.0), 'nodes': ['A', 'B']},
134      {'comp': DummyResist('R1', head_factor=1.0), 'nodes': ['B', 'C']},
135      {'comp': DummyResist('R2', head_factor=1.0), 'nodes': ['C', 'D']},
136      {'comp': DummyResist('R3', head_factor=1.0), 'nodes': ['D', 'A']},
137      {'comp': DummyResist('R4', head_factor=1.0), 'nodes': ['B', 'D']},
138    ],
139  )
140
141  assert len(network.cycleBase) == 2
142  # No fundamental loop should collapse to a single-segment equation.
143  assert all(len(loop) >= 3 for loop in network.cycleBase)
144  non_zero_counts = [int(np.count_nonzero(row)) for row in network.funcs['C']]
145  assert all(count >= 3 for count in non_zero_counts)
146
147def test_network_solution_invariant_to_segment_node_order() -> None:
148  def build(nodes3: list[str]) -> module_under_test.Network:
149    return module_under_test.Network(
150      name='InvariantOrder',
151      components=[
152        {'comp': DummyPump('P1', head_factor=4.0), 'nodes': ['A', 'B']},
153        {'comp': DummyResist('R1', head_factor=1.0), 'nodes': ['B', 'C']},
154        {'comp': DummyResist('R2', head_factor=1.0), 'nodes': nodes3},
155        {'comp': DummyPump('P2', head_factor=4.0), 'nodes': ['E', 'D']},
156        {'comp': DummyResist('R3', head_factor=1.0), 'nodes': ['E', 'F']},
157        {'comp': DummyResist('R4', head_factor=1.0), 'nodes': ['C', 'F']},
158        {'comp': DummyResist('R5', head_factor=1.0), 'nodes': ['A', 'F']},
159      ],
160    )
161
162  net_dc = build(['D', 'C'])
163  net_cd = build(['C', 'D'])
164
165  res_dc = {item['segment'].split(':', 1)[1]: item for item in net_dc.calcNetwork()}
166  res_cd = {item['segment'].split(':', 1)[1]: item for item in net_cd.calcNetwork()}
167
168  q_ab_dc = res_dc['A-B']['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
169  q_ab_cd = res_cd['A-B']['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
170  q_bc_dc = res_dc['B-C']['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
171  q_bc_cd = res_cd['B-C']['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
172  q_3_dc = res_dc['D-C']['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
173  q_3_cd = res_cd['C-D']['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude
174
175  h_ab_dc = res_dc['A-B']['H'].to(module_under_test.u.m).magnitude
176  h_ab_cd = res_cd['A-B']['H'].to(module_under_test.u.m).magnitude
177  h_bc_dc = res_dc['B-C']['H'].to(module_under_test.u.m).magnitude
178  h_bc_cd = res_cd['B-C']['H'].to(module_under_test.u.m).magnitude
179  h_3_dc = res_dc['D-C']['H'].to(module_under_test.u.m).magnitude
180  h_3_cd = res_cd['C-D']['H'].to(module_under_test.u.m).magnitude
181
182  assert q_ab_dc == pytest.approx(q_ab_cd)
183  assert q_bc_dc == pytest.approx(q_bc_cd)
184  assert abs(q_3_dc) == pytest.approx(abs(q_3_cd))
185  assert h_ab_dc == pytest.approx(h_ab_cd)
186  assert h_bc_dc == pytest.approx(h_bc_cd)
187  assert abs(h_3_dc) == pytest.approx(abs(h_3_cd))
188
189def test_network_validation_requires_source_and_resistance_in_loops() -> None:
190  with pytest.raises(ValueError, match='no energy source'):
191    module_under_test.Network(
192      components=[
193        {'comp': DummyResist('R1'), 'nodes': ['A', 'B']},
194        {'comp': DummyResist('R2'), 'nodes': ['B', 'C']},
195        {'comp': DummyResist('R3'), 'nodes': ['C', 'A']},
196      ]
197    )
198
199  with pytest.raises(ValueError, match='no resistance in loop'):
200    module_under_test.Network(
201      components=[
202        {'comp': DummyPump('P1'), 'nodes': ['A', 'B']},
203        {'comp': DummyPump('P2'), 'nodes': ['B', 'C']},
204        {'comp': DummyPump('P3'), 'nodes': ['C', 'A']},
205      ]
206    )
207
208def test_network_detects_duplicate_segments() -> None:
209  with pytest.raises(ValueError, match='Duplicate segment R1:A-B'):
210    module_under_test.Network(
211      components=[
212        {'comp': DummyResist('R1'), 'nodes': ['A', 'B']},
213        {'comp': DummyResist('R1'), 'nodes': ['A', 'B']},
214      ]
215    )
216
217def test_network_calc_network_returns_segment_results(monkeypatch) -> None:
218  network = module_under_test.Network(
219    name='Line',
220    components=[{'comp': DummyResist('R1', head_factor=2.0), 'nodes': ['A', 'B']}],
221  )
222  monkeypatch.setattr(
223    module_under_test,
224    'root',
225    lambda *args, **kwargs: types.SimpleNamespace(success=True, x=np.array([2.5]), message='ok', status=1),
226  )
227  result = network.calcNetwork(guess=1.2)
228  assert len(result) == 1
229  assert result[0]['segment'] == 'R1:A-B'
230  assert result[0]['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(2.5)
231  assert result[0]['H'].to(module_under_test.u.m).magnitude == pytest.approx(-5.0)
232  assert network.result == result
233
234def test_network_calc_network_uses_segment_sense_and_ports(monkeypatch) -> None:
235  comp_fwd = DummyDirectionalResist('R1', k_forward=2.0, k_reverse=7.0)
236  network_fwd = module_under_test.Network(
237    name='LineForward',
238    components=[{'comp': comp_fwd, 'nodes': ['A', 'B'], 'sense': +1}],
239  )
240  comp_rev = DummyDirectionalResist('R2', k_forward=2.0, k_reverse=7.0)
241  network_rev = module_under_test.Network(
242    name='LineReverse',
243    components=[{'comp': comp_rev, 'nodes': ['A', 'B'], 'sense': -1}],
244  )
245  def fake_root(func, x0, method='hybr'):  # pylint: disable=unused-argument
246    func(np.asarray(x0, dtype=float))
247    return types.SimpleNamespace(success=True, x=np.array([2.5]), message='ok', status=1)
248  monkeypatch.setattr(
249    module_under_test,
250    'root',
251    fake_root,
252  )
253
254  result_fwd = network_fwd.calcNetwork(guess=1.2)
255  result_rev = network_rev.calcNetwork(guess=1.2)
256
257  assert len(comp_fwd.calls) >= 1
258  assert len(comp_rev.calls) >= 1
259  assert comp_fwd.calls[-1][1:] == (+1, 1, 2)
260  assert comp_rev.calls[-1][1:] == (-1, 1, 2)
261  assert result_fwd[0]['H'].to(module_under_test.u.m).magnitude == pytest.approx(-5.0)
262  assert result_rev[0]['H'].to(module_under_test.u.m).magnitude == pytest.approx(-17.5)
263
264def test_network_calc_network_rejects_inconsistent_or_failed_systems(monkeypatch) -> None:
265  network = module_under_test.Network(
266    components=[
267      {'comp': DummyResist('R1'), 'nodes': ['A', 'B']},
268      {'comp': DummyResist('R2'), 'nodes': ['C', 'D']},
269    ]
270  )
271
272  with pytest.raises(ValueError, match='Inconsistent equation system'):
273    network.calcNetwork()
274
275  solvable = module_under_test.Network(
276    components=[{'comp': DummyResist('R3'), 'nodes': ['A', 'B']}],
277  )
278  monkeypatch.setattr(
279    module_under_test,
280    'root',
281    lambda *args, **kwargs: types.SimpleNamespace(success=False, message='failed', status=4),
282  )
283
284  with pytest.warns(RuntimeWarning, match='did not converge|failed'):
285    result = solvable.calcNetwork()
286
287  assert result[0]['Q'].to(module_under_test.u.m**3 / module_under_test.u.h).magnitude == pytest.approx(0.0)
288
289def test_network_string_helpers_include_topology_and_results() -> None:
290  network = _triangle_network()
291  network._result = [
292    {
293      'segment': 'P1:A-B',
294      'Q': 2.0 * module_under_test.u.m**3 / module_under_test.u.h,
295      'H': 8.0 * module_under_test.u.m,
296    }
297  ]
298
299  text = network.toString(detail=1)
300  text_formatted = f'{network:1}'
301  validation = network.networkValidationtoString()
302
303  assert 'Network "Loop"' in text
304  assert text_formatted == text
305  assert 'Nodes (3):' in text
306  assert 'A, B, C' in text
307  assert 'Segments (3):' in text
308  assert 'Adjacency (3):' in text
309  assert 'node' in text
310  assert 'links' in text
311  assert 'A    |' in text
312  assert 'P1:A-B' in text
313  assert '-> B' in text
314  assert 'SpanningTree' in text
315  assert 'segment' in text
316  assert 'path' in text
317  assert 'sense' in text
318  assert '+1' in text
319  assert 'CycleBase' in text
320  assert 'Loop 1:' in text
321  assert 'Functions: Combined incidence matrix (B:3) (C:1):' in text
322  assert 'Result:' in text
323  assert 'P1:A-B' in text
324  assert 'Loop 1:' in validation
325  assert 'Power' in validation
326  assert 'Resist' in validation
327
328def test_network_rebuilds_segments_when_valve_state_changes() -> None:
329  network = module_under_test.Network(
330    name='ValveLoop',
331    components=[
332      {'comp': DummyPump('P1', head_factor=4.0), 'nodes': ['A', 'B']},
333      {'comp': comp_valve.Comp_Valve_3W(name='V1', D=50, state=1), 'nodes': ['B', 'C', 'D']},
334      {'comp': DummyResist('R1', head_factor=1.5), 'nodes': ['C', 'A']},
335      {'comp': DummyResist('R2', head_factor=1.5), 'nodes': ['D', 'A']},
336    ],
337  )
338
339  def monkey_root(*_args, **_kwargs):
340    return types.SimpleNamespace(success=True, x=np.array([1.0, 1.0, 1.0, 1.0]), message='ok', status=1)
341
342  original_root = module_under_test.root
343  module_under_test.root = monkey_root
344
345  def _line_for(segment_name: str, text: str) -> str:
346    for line in text.splitlines():
347      if segment_name in line:
348        return line
349    return ''
350
351  try:
352    network.calcNetwork(guess=1.0)
353    keys_state_1 = set(network.segments.keys())
354    assert 'V1:B-C' in keys_state_1
355    assert 'V1:B-D' in keys_state_1
356    assert network.segments['V1:B-C']['use'] is True
357    assert network.segments['V1:B-D']['use'] is False
358    text_state_1 = network.segmentsString()
359    assert 'comp' in text_state_1
360    assert 'nodes' in text_state_1
361    assert 'type' in text_state_1
362    assert 'ports' in text_state_1
363    line_1_used = _line_for('V1:B-C', text_state_1)
364    line_1_unused = _line_for('V1:B-D', text_state_1)
365    assert '[unused]' not in line_1_used
366    assert line_1_used.startswith('   V1:B-C')
367    assert ' | ' in line_1_used
368    assert 'Comp_Valve_3W' in line_1_used
369    assert '1 → 3' in line_1_unused
370    assert line_1_unused.startswith('[[ V1:B-D')
371    assert line_1_unused.endswith(' ]]')
372
373    network.components[1]['comp'].state = 2
374    network.calcNetwork(guess=1.0)
375    keys_state_2 = set(network.segments.keys())
376    assert 'V1:B-D' in keys_state_2
377    assert 'V1:B-C' in keys_state_2
378    assert network.segments['V1:B-C']['use'] is False
379    assert network.segments['V1:B-D']['use'] is True
380    text_state_2 = network.segmentsString()
381    line_2_unused = _line_for('V1:B-C', text_state_2)
382    line_2_used = _line_for('V1:B-D', text_state_2)
383    assert line_2_unused.startswith('[[ V1:B-C')
384    assert line_2_unused.endswith(' ]]')
385    assert '[unused]' not in line_2_used
386    assert line_2_used.startswith('   V1:B-D')
387    assert ' | ' in line_2_used
388  finally:
389    module_under_test.root = original_root

Tests: test_path

  1'''Behavioral unit tests for fluidsolve.path.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,protected-access
  5
  6import inspect
  7import pytest
  8import fluidsolve.path as module_under_test
  9
 10def test_module_importable() -> None:
 11  assert module_under_test is not None
 12
 13@pytest.mark.parametrize('name', ['Path'])
 14def test_public_classes_exist(name: str) -> None:
 15  obj = getattr(module_under_test, name)
 16  assert inspect.isclass(obj)
 17
 18def test_public_functions_are_callable() -> None:
 19  public_function_names = [
 20    name
 21    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 22    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 23  ]
 24
 25  for name in public_function_names:
 26    obj = getattr(module_under_test, name)
 27    assert callable(obj)
 28
 29@pytest.mark.parametrize('name', ['Quantity', 'u'])
 30def test_public_variables_exist(name: str) -> None:
 31  assert hasattr(module_under_test, name)
 32
 33class DummyComp(module_under_test.flsb.Comp_Base):
 34  _group = 'Test'
 35  _part = 'Dummy'
 36  _nports = 2
 37
 38  def __init__(self, name: str, head: float=0.0, pressure: float=0.0) -> None:
 39    super().__init__(name=name)
 40    self.head = head
 41    self.pressure = pressure
 42    self.calls = []
 43
 44  def calcH(self, Q, sense: int=1, pin: int=1, pout: int=2):
 45    self.calls.append(('H', Q, sense, pin, pout))
 46    return self.head * sense * module_under_test.u.m
 47
 48  def calcP(self, Q, sense: int=1, pin: int=1, pout: int=2):
 49    self.calls.append(('P', Q, sense, pin, pout))
 50    return self.pressure * sense * module_under_test.u.bar
 51
 52class DummyValve(module_under_test.flsb.Comp_Base):
 53  _group = 'Valve'
 54  _part = 'Valve'
 55  _nports = 3
 56
 57  def __init__(self, name: str='V1', head: float=0.0) -> None:
 58    super().__init__(name=name)
 59    self.head = head
 60    self.calls = []
 61
 62  def calcH(self, Q, sense: int=1, pin: int=1, pout: int=2):
 63    self.calls.append(('H', Q, sense, pin, pout))
 64    return self.head * sense * module_under_test.u.m
 65
 66def test_path_defaults_and_accessors() -> None:
 67  path = module_under_test.Path(name='Loop')
 68
 69  assert path.name == 'Loop'
 70  assert not path.components
 71  assert path.componentsString() == '   <none>\n\n'
 72  assert str(path) == 'Path "Loop"\n'
 73
 74def test_path_add_components_normalizes_two_port_entries() -> None:
 75  comp = DummyComp('P1', head=2.0)
 76  path = module_under_test.Path(components=[{'comp': comp, 'sense': -1}])
 77
 78  item = path.getComp(0)
 79  assert item == {'comp': comp, 'sense': -1, 'pin': 1, 'pout': 2}
 80
 81  replacement = {'comp': comp, 'sense': 1, 'pin': 1, 'pout': 2}
 82  assert path.setComp(0, replacement) is replacement
 83  assert path.getComp(0) is replacement
 84
 85def test_path_add_components_requires_valid_entries() -> None:
 86  comp = DummyComp('P1')
 87  valve = DummyValve('V1')
 88
 89  with pytest.raises(ValueError, match='dict expected'):
 90    module_under_test.Path(components=['bad'])
 91  with pytest.raises(ValueError, match='missing key "comp"'):
 92    module_under_test.Path(components=[{'sense': 1}])
 93  with pytest.raises(ValueError, match='Unknown component'):
 94    module_under_test.Path(components=[{'comp': object()}])
 95  with pytest.raises(ValueError, match=r'sense must be \+1 or -1'):
 96    module_under_test.Path(components=[{'comp': comp, 'sense': 0}])
 97  with pytest.raises(ValueError, match='need pin and pout'):
 98    module_under_test.Path(components=[{'comp': valve}])
 99  with pytest.raises(ValueError, match='Invalid ports'):
100    module_under_test.Path(components=[{'comp': valve, 'pin': 0, 'pout': 4}])
101
102def test_path_add_components_keeps_explicit_ports_for_multiport_components() -> None:
103  valve = DummyValve('V1', head=1.5)
104  path = module_under_test.Path(components=[{'comp': valve, 'sense': -1, 'pin': 3, 'pout': 2}])
105
106  assert path.getComp(0) == {'comp': valve, 'sense': -1, 'pin': 3, 'pout': 2}
107
108def test_path_calcH_and_calcP_sum_component_results_with_sense() -> None:
109  comp1 = DummyComp('A', head=2.0, pressure=0.5)
110  comp2 = DummyComp('B', head=3.0, pressure=1.5)
111  path = module_under_test.Path(
112    components=[
113      {'comp': comp1, 'sense': 1},
114      {'comp': comp2, 'sense': -1},
115    ]
116  )
117
118  total_h = path.calcH(4 * module_under_test.u.m**3 / module_under_test.u.h, sense=-1)
119  total_p = path.calcP(4 * module_under_test.u.m**3 / module_under_test.u.h, sense=-1)
120
121  assert total_h.to(module_under_test.u.m).magnitude == pytest.approx(1.0)
122  assert total_p.to(module_under_test.u.bar).magnitude == pytest.approx(1.0)
123  assert comp1.calls[0][2:] == (-1, 1, 2)
124  assert comp2.calls[0][2:] == (1, 1, 2)
125  assert comp1.calls[1][2:] == (-1, 1, 2)
126  assert comp2.calls[1][2:] == (1, 1, 2)
127
128def test_path_calcHprofile_returns_incremental_and_total_points() -> None:
129  comp1 = DummyComp('A', head=2.0)
130  comp2 = DummyComp('B', head=3.0)
131  path = module_under_test.Path(components=[{'comp': comp1}, {'comp': comp2}])
132
133  points = path.calcHprofile(5, incr=True)
134
135  assert [point.name for point in points] == ['0:A', '1:B', 'Tot']
136  assert [point.Qmag for point in points] == [5.0, 5.0, 5.0]
137  assert [point.Hmag for point in points] == [2.0, 5.0, 5.0]
138
139def test_path_calcHprofile_non_incremental_uses_component_head_per_step() -> None:
140  comp1 = DummyComp('A', head=2.0)
141  comp2 = DummyComp('B', head=3.0)
142  path = module_under_test.Path(components=[{'comp': comp1}, {'comp': comp2}])
143
144  points = path.calcHprofile(5, incr=False, sense=-1)
145
146  assert [point.Hmag for point in points] == [-2.0, -3.0, -5.0]
147  assert comp1.calls[0][2:] == (-1, 1, 2)
148  assert comp2.calls[0][2:] == (-1, 1, 2)
149
150def test_path_to_string_formats_component_listing_and_detail() -> None:
151  comp1 = DummyComp('PumpA', head=2.0)
152  valve = DummyValve('Valve1', head=1.0)
153  path = module_under_test.Path(
154    name='Loop',
155    components=[
156      {'comp': comp1, 'sense': 1},
157      {'comp': valve, 'sense': -1, 'pin': 3, 'pout': 1},
158    ]
159  )
160
161  text = path.toString(detail=1)
162
163  assert 'Path "Loop"' in text
164  assert 'Components (2):' in text
165  assert 'idx | Comp' in text
166  assert 'PumpA' in text
167  assert 'Valve1' in text
168  assert ' |  \u2192  | ' in text
169  assert ' |  \u2190  | ' in text
170  assert '3 -> 1' in text

Tests: test_plotext

  1'''Behavioral unit tests for fluidsolve.plotext.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,protected-access,unused-argument
  5
  6import inspect
  7from types import SimpleNamespace
  8import numpy as np
  9import pytest
 10import fluidsolve.plotext as module_under_test
 11
 12def test_module_importable() -> None:
 13  assert module_under_test is not None
 14
 15@pytest.mark.parametrize('name', ['PlotQHcurve', 'PlotSimple'])
 16def test_public_classes_exist(name: str) -> None:
 17  obj = getattr(module_under_test, name)
 18  assert inspect.isclass(obj)
 19
 20def test_public_functions_are_callable() -> None:
 21  public_function_names = [
 22    name
 23    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 24    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 25  ]
 26
 27  for name in public_function_names:
 28    obj = getattr(module_under_test, name)
 29    assert callable(obj)
 30
 31@pytest.mark.parametrize('name', ['Quantity', 'u'])
 32def test_public_variables_exist(name: str) -> None:
 33  assert hasattr(module_under_test, name)
 34
 35class DummyFigure:
 36  def __init__(self, **kwargs):
 37    self.kwargs = kwargs
 38    self.hw = kwargs.get('hw', 50)
 39    self.nrw = kwargs.get('nrw', 1)
 40    self.ncw = kwargs.get('ncw', 1)
 41    self.prepare_show_calls = 0
 42    self.show_calls = 0
 43    self.update_calls = 0
 44    self.update_data_calls = 0
 45
 46  def prepareShow(self) -> None:
 47    self.prepare_show_calls += 1
 48
 49  def show(self) -> None:
 50    self.show_calls += 1
 51
 52  def update(self) -> None:
 53    self.update_calls += 1
 54
 55  def updateData(self) -> None:
 56    self.update_data_calls += 1
 57
 58class DummyGraph:
 59  def __init__(self, figure, r=0, c=0):
 60    self.figure = figure
 61    self.r = r
 62    self.c = c
 63    self.xaxis = None
 64    self.yaxis = None
 65    self.grid = None
 66
 67  def setXAxis(self, **kwargs) -> None:
 68    self.xaxis = kwargs
 69
 70  def setYAxis(self, **kwargs) -> None:
 71    self.yaxis = kwargs
 72
 73  def setGrid(self, **kwargs) -> None:
 74    self.grid = kwargs
 75
 76class DummyCurve:
 77  def __init__(self, graph, **kwargs):
 78    self.graph = graph
 79    self.kwargs = kwargs
 80    self.x = None
 81    self.y = None
 82
 83class DummyAnnotation:
 84  def __init__(self, graph, **kwargs):
 85    self.graph = graph
 86    self.kwargs = kwargs
 87    self.x = None
 88    self.y = None
 89    self.label = None
 90
 91class DummyButton:
 92  def __init__(self, figure, **kwargs):
 93    self.figure = figure
 94    self.kwargs = kwargs
 95
 96class DummySlider:
 97  def __init__(self, figure, **kwargs):
 98    self.figure = figure
 99    self.kwargs = kwargs
100    self.reset_calls = 0
101    self.widget = SimpleNamespace(reset=self._reset)
102
103  def _reset(self) -> None:
104    self.reset_calls += 1
105
106def _install_plot_stubs(monkeypatch) -> None:
107  monkeypatch.setattr(module_under_test.flsp, 'PlotFigure', DummyFigure)
108  monkeypatch.setattr(module_under_test.flsp, 'PlotGraph', DummyGraph)
109  monkeypatch.setattr(module_under_test.flsp, 'PlotCurve', DummyCurve)
110  monkeypatch.setattr(module_under_test.flsp, 'PlotAnnotation', DummyAnnotation)
111  monkeypatch.setattr(module_under_test.flsp, 'PlotButton', DummyButton)
112  monkeypatch.setattr(module_under_test.flsp, 'PlotSlider', DummySlider)
113
114def test_plot_qhcurve_initializes_graph_axes_and_optional_widgets(monkeypatch) -> None:
115  _install_plot_stubs(monkeypatch)
116
117  plotter = module_under_test.PlotQHcurve(
118    xmin=1,
119    xmax=9,
120    xstep=2,
121    ymin=0,
122    ymax=12,
123    ystep=3,
124    xlabel='Flow',
125    ylabel='Head',
126    sliders=[{'label': 'Speed'}],
127    title='Plot',
128  )
129  fig_kwargs = getattr(plotter._fig, 'kwargs')
130  graph_xaxis = getattr(plotter._graph, 'xaxis')
131  graph_yaxis = getattr(plotter._graph, 'yaxis')
132  graph_grid = getattr(plotter._graph, 'grid')
133
134  assert isinstance(plotter._fig, DummyFigure)
135  assert isinstance(plotter._graph, DummyGraph)
136  assert fig_kwargs == {'title': 'Plot'}
137  assert graph_xaxis == {'vmin': 1, 'vmax': 9, 'vstep': 2, 'labeltxt': 'Flow'}
138  assert graph_yaxis == {'vmin': 0, 'vmax': 12, 'vstep': 3, 'labeltxt': 'Head'}
139  assert graph_grid == {'axis': 'both'}
140  assert plotter._fig.hw == 60
141  assert plotter._fig.nrw == 2
142  assert plotter._fig.ncw == 10
143  assert isinstance(plotter._buttonreset, DummyButton)
144  assert len(plotter._sliders) == 1
145  assert getattr(plotter._sliders[0], 'kwargs')['label'] == 'Speed'
146  assert getattr(plotter._sliders[0], 'kwargs')['r'] == 1
147  assert getattr(plotter._sliders[0], 'kwargs')['c'] == '0:9'
148
149def test_prepare_show_creates_plot_objects_and_populates_data(monkeypatch) -> None:
150  _install_plot_stubs(monkeypatch)
151
152  class DummyPump:
153    def __init__(self):
154      self.Qb = SimpleNamespace(magnitude=1.0)
155      self.Qe = SimpleNamespace(magnitude=4.0)
156
157    def calcH(self, flow, sense):
158      return SimpleNamespace(magnitude=np.array([9.0, 7.0, 0.0, -1.0]))
159
160  class DummyCircuit:
161    def calcH(self, flow, sense):
162      return SimpleNamespace(magnitude=np.array([-1.0, -2.0, -3.0, -4.0]))
163
164  class DummyPoint:
165    def __init__(self, name: str, qmag: float, hmag: float):
166      self.name = name
167      self.Qmag = qmag
168      self.Hmag = hmag
169      self.update_calls = 0
170
171    def update(self) -> None:
172      self.update_calls += 1
173
174  monkeypatch.setattr(module_under_test.np, 'linspace', lambda start, stop, count: np.array([start, 2.0, 3.0, stop]))
175  monkeypatch.setattr(module_under_test.np, 'argmax', lambda values: next((i for i, value in enumerate(values) if value), 0))
176
177  wpoint = DummyPoint('WP1', 2.5, 6.0)
178  spoint = DummyPoint('SP1', 1.5, 3.0)
179  plotter = module_under_test.PlotQHcurve(
180    pumps=[DummyPump()],
181    circuits=[DummyCircuit()],
182    wpoints=[wpoint],
183    spoints=[spoint],
184    npts=4,
185    Qmax=8,
186  )
187
188  plotter.prepareShow()
189  prepare_show_calls = getattr(plotter._fig, 'prepare_show_calls')
190
191  assert plotter._prepare is False
192  assert prepare_show_calls == 1
193  assert len(plotter._curvepumps) == 1
194  assert len(plotter._curvecircuits) == 1
195  assert len(plotter._curvewpts) == 1
196  assert len(plotter._curvespts) == 1
197  assert np.allclose(np.asarray(plotter._curvepumps[0].x), np.asarray([1.0, 2.0]))
198  assert np.allclose(np.asarray(plotter._curvepumps[0].y), np.asarray([9.0, 7.0]))
199  assert np.allclose(np.asarray(plotter._curvecircuits[0].x), np.asarray([0.001, 2.0, 3.0, 8.0]))
200  assert np.allclose(np.asarray(plotter._curvecircuits[0].y), np.asarray([1.0, 2.0, 3.0, 4.0]))
201  assert plotter._curvewpts[0].x == [2.5]
202  assert plotter._curvewpts[0].y == [6.0]
203  assert plotter._annotationwpts[0].label == ['WP1']
204  assert plotter._curvespts[0].x == [1.5]
205  assert plotter._curvespts[0].y == [3.0]
206  assert plotter._annotationspts[0].label == ['SP1']
207  assert wpoint.update_calls == 1
208  assert spoint.update_calls == 1
209
210def test_prepare_show_runs_only_once_and_show_delegates(monkeypatch) -> None:
211  _install_plot_stubs(monkeypatch)
212
213  plotter = module_under_test.PlotQHcurve()
214
215  plotter.prepareShow()
216  plotter.prepareShow()
217  plotter.show()
218  prepare_show_calls = getattr(plotter._fig, 'prepare_show_calls')
219  show_calls = getattr(plotter._fig, 'show_calls')
220
221  assert prepare_show_calls == 1
222  assert show_calls == 1
223
224def test_update_and_update_data_delegate_to_figure(monkeypatch) -> None:
225  _install_plot_stubs(monkeypatch)
226  plotter = module_under_test.PlotQHcurve()
227
228  calls = []
229  monkeypatch.setattr(plotter, '_calcAndUpdate', lambda: calls.append('calc'))
230
231  plotter.update()
232  plotter.updateData()
233  update_calls = getattr(plotter._fig, 'update_calls')
234  update_data_calls = getattr(plotter._fig, 'update_data_calls')
235
236  assert calls == ['calc', 'calc']
237  assert update_calls == 1
238  assert update_data_calls == 1
239
240def test_reset_controls_resets_each_slider(monkeypatch) -> None:
241  _install_plot_stubs(monkeypatch)
242  plotter = module_under_test.PlotQHcurve(sliders=[{'label': 'A'}, {'label': 'B'}])
243
244  plotter._resetControls(event=None)
245
246  assert [slider.reset_calls for slider in plotter._sliders] == [1, 1]
247
248
249def test_plot_simple_initializes_single_graph_with_provided_data(monkeypatch) -> None:
250  _install_plot_stubs(monkeypatch)
251
252  plotter = module_under_test.PlotSimple(
253    x=[1, 2],
254    y=[3, 4],
255    type='line',
256    xlabel='X',
257    ylabel='Y',
258    xmin=0,
259    xmax=10,
260    xstep=2,
261    ymin=0,
262    ymax=20,
263    ystep=5,
264    title='Simple',
265  )
266  plotter.prepareShow()
267
268  assert isinstance(plotter._fig, DummyFigure)
269  assert isinstance(plotter._graph, DummyGraph)
270  assert getattr(plotter._fig, 'kwargs') == {'title': 'Simple'}
271  assert getattr(plotter._graph, 'xaxis') == {'vmin': 0, 'vmax': 10, 'vstep': 2, 'labeltxt': 'X'}
272  assert getattr(plotter._graph, 'yaxis') == {'vmin': 0, 'vmax': 20, 'vstep': 5, 'labeltxt': 'Y'}
273  assert getattr(plotter._graph, 'grid') == {'axis': 'both'}
274  assert isinstance(plotter._curve, DummyCurve)
275  assert getattr(plotter._curve, 'kwargs')['x'] == [1, 2]
276  assert getattr(plotter._curve, 'kwargs')['y'] == [3, 4]
277
278
279def test_plot_simple_set_data_and_update_paths(monkeypatch) -> None:
280  _install_plot_stubs(monkeypatch)
281
282  plotter = module_under_test.PlotSimple(x=[1], y=[2])
283  plotter.prepareShow()
284  plotter.setData([2, 3], [4, 5])
285  plotter.update()
286  plotter.updateData()
287
288  assert plotter._curve.x == [2, 3]
289  assert plotter._curve.y == [4, 5]
290  assert getattr(plotter._fig, 'update_calls') == 1
291  assert getattr(plotter._fig, 'update_data_calls') == 1

Tests: test_plotlib

  1'''Behavioral unit tests for fluidsolve.plotlib.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,protected-access,unused-argument,disallowed-name
  5
  6import inspect
  7from types import SimpleNamespace
  8import pytest
  9import fluidsolve.plotlib as module_under_test
 10
 11class DummyParent:
 12  def __init__(self, axes=None):
 13    self.axes = axes if axes is not None else DummyAxes()
 14    self._xaxis2 = None
 15    self._yaxis2 = None
 16
 17  def addCurve(self, _curve):
 18    return 0
 19
 20  def addAnnotation(self, _annotation):
 21    return 0
 22
 23  def getAxes(self, axis='main'):
 24    if axis in ('main', 'x1', 'y1'):
 25      return self.axes
 26    if axis == 'x2':
 27      return None if self._xaxis2 is None else self._xaxis2.axes
 28    if axis == 'y2':
 29      return None if self._yaxis2 is None else self._yaxis2.axes
 30    raise ValueError(f'Invalid axis {axis}')
 31
 32  def getAllAxes(self):
 33    axes = [self.axes]
 34    for axis in (self._xaxis2, self._yaxis2):
 35      if axis is not None and axis.axes is not None and axis.axes not in axes:
 36        axes.append(axis.axes)
 37    return axes
 38
 39def test_module_importable() -> None:
 40  assert module_under_test is not None
 41
 42@pytest.mark.parametrize('name', ['PlotAnnotation', 'PlotAxis', 'PlotButton', 'PlotCurve', 'PlotFigure', 'PlotGraph', 'PlotGrid', 'PlotLine', 'PlotSlider'])
 43def test_public_classes_exist(name: str) -> None:
 44  obj = getattr(module_under_test, name)
 45  assert inspect.isclass(obj)
 46
 47def test_public_functions_are_callable() -> None:
 48  public_function_names = [
 49    name
 50    for name, obj in inspect.getmembers(module_under_test, inspect.isfunction)
 51    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 52  ]
 53
 54  for name in public_function_names:
 55    obj = getattr(module_under_test, name)
 56    assert callable(obj)
 57
 58def test_public_variables_exist() -> None:
 59  public_variable_names = [
 60    name
 61    for name, obj in inspect.getmembers(module_under_test)
 62    if not name.startswith('_')
 63    and not inspect.isclass(obj)
 64    and not inspect.isfunction(obj)
 65    and getattr(obj, '__module__', module_under_test.__name__) == module_under_test.__name__
 66  ]
 67
 68  for name in public_variable_names:
 69    assert hasattr(module_under_test, name)
 70
 71class DummyCanvas:
 72  def __init__(self):
 73    self.draw_idle_calls = 0
 74
 75  def draw_idle(self) -> None:
 76    self.draw_idle_calls += 1
 77
 78class DummyFigureObject:
 79  def __init__(self):
 80    self.canvas = DummyCanvas()
 81    self.suptitle_calls = []
 82    self.subplots = []
 83
 84  def suptitle(self, title: str, **kwargs) -> None:
 85    self.suptitle_calls.append((title, kwargs))
 86
 87  def add_subplot(self, spec, **kwargs):
 88    axis = DummyAxes()
 89    self.subplots.append((spec, kwargs, axis))
 90    return axis
 91
 92class DummyGridSpec:
 93  def __init__(self, nrows: int, ncols: int, figure=None):
 94    self.nrows = nrows
 95    self.ncols = ncols
 96    self.figure = figure
 97
 98  def __getitem__(self, item):
 99    return item
100
101class DummyAxes:
102  def __init__(self):
103    self.calls = []
104    self.xaxis = SimpleNamespace(set_minor_locator=lambda loc: self.calls.append(('xminor', loc)))
105    self.yaxis = SimpleNamespace(set_minor_locator=lambda loc: self.calls.append(('yminor', loc)))
106    self.legend_handles = []
107    self.legend_labels = []
108
109  def set_title(self, *args, **kwargs):
110    self.calls.append(('set_title', args, kwargs))
111
112  def set_xlim(self, *args, **kwargs):
113    self.calls.append(('set_xlim', args, kwargs))
114
115  def set_xticks(self, *args, **kwargs):
116    self.calls.append(('set_xticks', args, kwargs))
117
118  def set_ylim(self, *args, **kwargs):
119    self.calls.append(('set_ylim', args, kwargs))
120
121  def set_yticks(self, *args, **kwargs):
122    self.calls.append(('set_yticks', args, kwargs))
123
124  def set_xlabel(self, *args, **kwargs):
125    self.calls.append(('set_xlabel', args, kwargs))
126
127  def set_ylabel(self, *args, **kwargs):
128    self.calls.append(('set_ylabel', args, kwargs))
129
130  def tick_params(self, *args, **kwargs):
131    self.calls.append(('tick_params', args, kwargs))
132
133  def sharex(self, other):
134    self.calls.append(('sharex', other))
135
136  def sharey(self, other):
137    self.calls.append(('sharey', other))
138
139  def twinx(self):
140    twin = DummyAxes()
141    self.calls.append(('twinx', twin))
142    return twin
143
144  def twiny(self):
145    twin = DummyAxes()
146    self.calls.append(('twiny', twin))
147    return twin
148
149  def legend(self, **kwargs):
150    self.calls.append(('legend', kwargs))
151
152  def get_legend_handles_labels(self):
153    return self.legend_handles, self.legend_labels
154
155  def grid(self, **kwargs):
156    self.calls.append(('grid', kwargs))
157
158  def annotate(self, *args, **kwargs):
159    self.calls.append(('annotate', args, kwargs))
160    return SimpleNamespace(remove=lambda: self.calls.append(('remove_anno',)))
161
162  def plot(self, x, y, **kwargs):
163    line = SimpleNamespace(
164      set_xdata=lambda data: self.calls.append(('line_x', data)),
165      set_ydata=lambda data: self.calls.append(('line_y', data)),
166    )
167    self.calls.append(('plot', x, y, kwargs))
168    return [line]
169
170  def scatter(self, x, y, **kwargs):
171    obj = SimpleNamespace(set_offsets=lambda data: self.calls.append(('scatter_offsets', data.tolist() if hasattr(data, 'tolist') else data)))
172    self.calls.append(('scatter', x, y, kwargs))
173    return obj
174
175  def bar(self, x, **kwargs):
176    bar = SimpleNamespace(
177      set_xdata=lambda data: self.calls.append(('bar_x', data)),
178      set_ydata=lambda data: self.calls.append(('bar_y', data)),
179    )
180    self.calls.append(('bar', x, kwargs))
181    return [bar]
182
183class DummyWidget:
184  def __init__(self):
185    self.callbacks = []
186
187  def on_clicked(self, callback):
188    self.callbacks.append(callback)
189
190  def on_changed(self, callback):
191    self.callbacks.append(callback)
192
193def test_plotfigure_prepare_show_and_update_paths(monkeypatch) -> None:
194  created_figures = []
195
196  def fake_figure(**kwargs):
197    fig = DummyFigureObject()
198    created_figures.append((kwargs, fig))
199    return fig
200
201  monkeypatch.setattr(module_under_test.plt, 'figure', fake_figure)
202  monkeypatch.setattr(module_under_test.gridspec, 'GridSpec', DummyGridSpec)
203
204  fig = module_under_test.PlotFigure(title='Title', nr=2, nc=3, nrw=2, ncw=4)
205  graph = SimpleNamespace(show_calls=0, update_calls=0, update_data_calls=0)
206  graph.show = lambda: setattr(graph, 'show_calls', graph.show_calls + 1)
207  graph.update = lambda: setattr(graph, 'update_calls', graph.update_calls + 1)
208  graph.updateData = lambda: setattr(graph, 'update_data_calls', graph.update_data_calls + 1)
209  fig.addGraph(graph)
210  fig.addButton(SimpleNamespace(show=lambda: None))
211  fig.addSlider(SimpleNamespace(show=lambda: None))
212
213  fig.prepareShow()
214  fig.update()
215  fig.updateData()
216
217  assert len(created_figures) == 2
218  assert graph.show_calls == 1
219  assert graph.update_calls == 1
220  assert graph.update_data_calls == 1
221  assert getattr(fig.figure, 'suptitle_calls')[0][0] == 'Title'
222  assert fig.gridspec.nrows == 2
223  assert fig.gridspec.ncols == 3
224  assert fig.gridspec_widgets.nrows == 2
225  assert fig.gridspec_widgets.ncols == 4
226  assert getattr(fig.figure, 'canvas').draw_idle_calls == 2
227
228def test_plotgraph_show_creates_axes_and_invokes_children() -> None:
229  fig = module_under_test.PlotFigure()
230  fig._fig = DummyFigureObject()
231  fig._gs = DummyGridSpec(1, 1, figure=fig._fig)
232
233  graph = module_under_test.PlotGraph(fig, r='0:1', c=':')
234  graph.setXAxis(vmin=0, vmax=10, vstep=5, labeltxt='Q')
235  graph.setYAxis(vmin=0, vmax=10, vstep=5, labeltxt='H')
236  graph.setGrid(axis='both')
237
238  curve = SimpleNamespace(show_calls=0, update_calls=0, update_data_calls=0)
239  curve.show = lambda: setattr(curve, 'show_calls', curve.show_calls + 1)
240  curve.update = lambda: setattr(curve, 'update_calls', curve.update_calls + 1)
241  curve.updateData = lambda: setattr(curve, 'update_data_calls', curve.update_data_calls + 1)
242  annotation = SimpleNamespace(show_calls=0, update_calls=0, update_data_calls=0)
243  annotation.show = lambda: setattr(annotation, 'show_calls', annotation.show_calls + 1)
244  annotation.update = lambda: setattr(annotation, 'update_calls', annotation.update_calls + 1)
245  annotation.updateData = lambda: setattr(annotation, 'update_data_calls', annotation.update_data_calls + 1)
246  graph._curves.append(curve)
247  graph._annotations.append(annotation)
248
249  graph.show()
250  graph.update()
251  graph.updateData()
252
253  assert graph.axes is not None
254  assert curve.show_calls == 1
255  assert curve.update_calls == 1
256  assert curve.update_data_calls == 1
257  assert annotation.show_calls == 1
258  assert annotation.update_calls == 1
259  assert annotation.update_data_calls == 1
260
261def test_plotcurve_line_scatter_bar_show_and_update_data() -> None:
262  parent = DummyParent()
263
264  line = module_under_test.PlotCurve(parent, type='line', x=[1, 2], y=[3, 4])
265  line.show()
266  line.x = [2, 3]
267  line.y = [4, 5]
268  line.updateData()
269
270  scatter = module_under_test.PlotCurve(parent, type='scatter', x=[1, 2], y=[3, 4])
271  scatter.show()
272  scatter.x = [2, 3]
273  scatter.y = [5, 6]
274  scatter.updateData()
275
276  bar = module_under_test.PlotCurve(parent, type='bar', x=[1, 2], y=[3, 4])
277  bar.show()
278  bar.x = [2, 3]
279  bar.y = [6, 7]
280  bar.updateData()
281
282  calls = parent.axes.calls
283  assert any(c[0] == 'plot' for c in calls)
284  assert any(c[0] == 'scatter' for c in calls)
285  assert any(c[0] == 'bar' for c in calls)
286  assert any(c[0] == 'line_x' for c in calls)
287  assert any(c[0] == 'scatter_offsets' for c in calls)
288  assert any(c[0] == 'bar_y' for c in calls)
289
290def test_plotcurve_can_target_secondary_y_axis() -> None:
291  parent = DummyParent()
292  parent._yaxis2 = module_under_test.PlotAxis(parent, type='y2', labeltxt='Q')
293  parent._yaxis2.show()
294
295  curve = module_under_test.PlotCurve(parent, type='line', x=[1, 2], y=[3, 4], axis='y2', label='Flow')
296  curve.show()
297
298  assert any(c[0] == 'twinx' for c in parent.axes.calls)
299  twin_axes = parent._yaxis2.axes
300  assert twin_axes is not None
301  assert any(c[0] == 'plot' and c[3]['label'] == 'Flow' for c in twin_axes.calls)
302
303def test_plotcurve_raises_when_secondary_axis_is_missing() -> None:
304  parent = DummyParent()
305
306  with pytest.raises(ValueError, match='Axis y2 is not configured'):
307    module_under_test.PlotCurve(parent, type='line', x=[1], y=[2], axis='y2').show()
308
309def test_plotannotation_validates_and_updates_annotations() -> None:
310  parent = DummyParent()
311  annotation = module_under_test.PlotAnnotation(parent, x=[1, 2], y=[3, 4], label=['A', 'B'], xtoggle=1)
312  annotation.show()
313  annotation.updateData()
314
315  assert len(annotation._annotations) == 2
316
317  bad = module_under_test.PlotAnnotation(parent, x=[1], y=[2, 3], label=['A'])
318  with pytest.raises(ValueError, match='Size of x list'):
319    bad.show()
320
321def test_plotaxis_grid_and_extra_validation() -> None:
322  graph = DummyParent()
323  axis = module_under_test.PlotAxis(graph, type='x1', vmin=0, vmax=10, vstep=5, labeltxt='Q')
324  axis.show()
325
326  grid = module_under_test.PlotGrid(graph, axis='both', color='gray')
327  grid.show()
328
329  with pytest.raises(ValueError, match='Need vmin and vmax when vstep is provided'):
330    module_under_test.PlotAxis(graph, type='y1', vmin=0, vstep=1).show()
331  with pytest.raises(ValueError, match='Invalid extra'):
332    grid.setExtra('bad', alpha=0.5)
333
334def test_plotaxis_applies_manual_limits_even_when_auto_ticks_remain_enabled() -> None:
335  graph = DummyParent()
336
337  axis = module_under_test.PlotAxis(graph, type='y1', vmax=30)
338  axis.show()
339
340  assert ('set_ylim', (), {'bottom': None, 'top': 30}) in graph.axes.calls
341
342def test_plotlegend_collects_entries_from_secondary_axes() -> None:
343  graph = module_under_test.PlotGraph(module_under_test.PlotFigure(), r=0, c=0)
344  graph._ax = DummyAxes()
345  graph.setYAxis2(labeltxt='Q')
346  graph._yaxis2.show()
347  graph.axes.legend_handles = ['h1']
348  graph.axes.legend_labels = ['Level']
349  graph.getAxes('y2').legend_handles = ['h2']
350  graph.getAxes('y2').legend_labels = ['Flow']
351
352  legend = module_under_test.PlotLegend(graph, loc='upper right')
353  legend.show()
354
355  legend_calls = [call for call in graph.axes.calls if call[0] == 'legend']
356  assert legend_calls
357  kwargs = legend_calls[-1][1]
358  assert kwargs['handles'] == ['h1', 'h2']
359  assert kwargs['labels'] == ['Level', 'Flow']
360
361def test_plotbutton_and_plotslider_show_bind_callbacks(monkeypatch) -> None:
362  monkeypatch.setattr(module_under_test, 'Button', lambda *args, **kwargs: DummyWidget())
363  monkeypatch.setattr(module_under_test, 'Slider', lambda *args, **kwargs: DummyWidget())
364
365  fig = module_under_test.PlotFigure()
366  fig._figwidgets = DummyFigureObject()
367  fig._gswidgets = DummyGridSpec(2, 2, figure=fig._figwidgets)
368
369  button = module_under_test.PlotButton(fig, r=0, c=0, label='Run', fun=lambda event: None)
370  slider = module_under_test.PlotSlider(fig, r=1, c=0, label='S', vmin=0, vmax=10, fun=lambda value: None)
371  button.show()
372  slider.show()
373  button_widget = getattr(button, '_widget')
374  slider_widget = getattr(slider, '_widget')
375  button_callbacks = getattr(button_widget, 'callbacks')
376  slider_callbacks = getattr(slider_widget, 'callbacks')
377
378  assert button_widget is not None
379  assert slider_widget is not None
380  assert len(button_callbacks) == 1
381  assert len(slider_callbacks) == 1

Tests: test_util

  1'''Behavioral unit tests for fluidsolve.util.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,protected-access,unused-argument
  5
  6import inspect
  7import pytest
  8import fluidsolve.util as module_under_test
  9
 10u = module_under_test.u
 11
 12def test_module_importable() -> None:
 13  assert module_under_test is not None
 14
 15def test_public_classes_exist() -> None:
 16  public_class_names = [
 17    name
 18    for name, obj in inspect.getmembers(module_under_test, inspect.isclass)
 19    if obj.__module__ == module_under_test.__name__ and not name.startswith('_')
 20  ]
 21
 22  for name in public_class_names:
 23    obj = getattr(module_under_test, name)
 24    assert inspect.isclass(obj)
 25
 26@pytest.mark.parametrize('name', ['CvtoK', 'CvtoKv', 'FdtoK', 'Htop', 'KtoCv', 'KtoFd', 'KtoH', 'KtoKv', 'Ktop', 'KvtoCv', 'KvtoK', 'Qtov', 'calcCurve', 'calcOrifice', 'calcOrifice2', 'ptoH', 'vtoQ'])
 27def test_public_functions_are_callable(name: str) -> None:
 28  obj = getattr(module_under_test, name)
 29  assert callable(obj)
 30
 31@pytest.mark.parametrize('name', ['Quantity', 'u'])
 32def test_public_variables_exist(name: str) -> None:
 33  assert hasattr(module_under_test, name)
 34
 35def test_k_fd_conversions_roundtrip() -> None:
 36  k_loss = 3.2
 37  length = 12 * u.m
 38  diameter = 80 * u.mm
 39
 40  fd = module_under_test.KtoFd(k_loss, length, diameter)
 41  k_back = module_under_test.FdtoK(fd, length, diameter)
 42
 43  assert k_back == pytest.approx(k_loss)
 44
 45def test_kv_cv_and_k_conversions_roundtrip() -> None:
 46  diameter = 50 * u.mm
 47  kv = 25 * u.m**3 / u.h
 48
 49  cv = module_under_test.KvtoCv(kv)
 50  kv_back = module_under_test.CvtoKv(cv)
 51
 52  assert kv_back.to(u.m**3 / u.h).magnitude == pytest.approx(kv.to(u.m**3 / u.h).magnitude)
 53  with pytest.raises(Exception):
 54    module_under_test.KtoKv(15.0, diameter)
 55
 56def test_head_pressure_and_velocity_flow_conversions_are_consistent() -> None:
 57  rho = 998 * u.kg / u.m**3
 58  head = 12 * u.m
 59  pressure = module_under_test.Htop(head, rho)
 60  head_back = module_under_test.ptoH(pressure, rho)
 61
 62  flow = 9 * u.m**3 / u.h
 63  diameter = 100 * u.mm
 64  velocity = module_under_test.Qtov(flow, diameter)
 65  flow_back = module_under_test.vtoQ(velocity, diameter)
 66
 67  assert head_back.to(u.m).magnitude == pytest.approx(head.to(u.m).magnitude)
 68  assert flow_back.to(u.m**3 / u.h).magnitude == pytest.approx(flow.to(u.m**3 / u.h).magnitude)
 69
 70def test_ktoh_and_ktop_match_via_density_conversion() -> None:
 71  k_loss = 6.0
 72  velocity = 1.5 * u.m / u.s
 73  rho = 1000 * u.kg / u.m**3
 74
 75  head = module_under_test.KtoH(k_loss, velocity)
 76  pressure_from_head = module_under_test.Htop(head, rho)
 77  pressure_direct = module_under_test.Ktop(k_loss, velocity, rho)
 78
 79  assert pressure_from_head.to(u.bar).magnitude == pytest.approx(pressure_direct.to(u.bar).magnitude)
 80
 81def test_calc_curve_filters_out_of_bounds_and_accepts_quantity_output() -> None:
 82  x_pts, y_pts = module_under_test.calcCurve(
 83    xb=0,
 84    xe=10,
 85    xn=6,
 86    yfun=lambda x: (x - 5) * u.m,
 87    yb=-2,
 88    ye=2,
 89  )
 90
 91  assert list(x_pts) == [4.0, 6.0]
 92  assert list(y_pts) == [-1.0, 1.0]
 93
 94def test_calc_orifice_branches_and_solver_arguments(monkeypatch) -> None:
 95  captured = {}
 96
 97  def fake_solver(**kwargs):
 98    captured.clear()
 99    captured.update(kwargs)
100    if kwargs.get('D2') is None:
101      return 22 * u.mm
102    if kwargs.get('P1') is None:
103      return 2.4 * u.bar
104    if kwargs.get('P2') is None:
105      return 1.1 * u.bar
106    if kwargs.get('m') is None:
107      return 500 * u.kg / u.h
108    return 42.0
109
110  monkeypatch.setattr(module_under_test.fu, 'differential_pressure_meter_solver', fake_solver)
111
112  out_orifice = module_under_test.calcOrifice(Q=3, d=50, Pin=2, Pout=1)
113  out_pin = module_under_test.calcOrifice(Q=3, d=50, orifice=25, Pout=1)
114  out_pout = module_under_test.calcOrifice(Q=3, d=50, orifice=25, Pin=2)
115  out_q = module_under_test.calcOrifice(d=50, orifice=25, Pin=2, Pout=1)
116
117  assert out_orifice.to(u.mm).magnitude == pytest.approx(22)
118  assert out_pin.to(u.bar).magnitude == pytest.approx(2.4)
119  assert out_pout.to(u.bar).magnitude == pytest.approx(1.1)
120  assert out_q.to(u.m**3 / u.h).magnitude > 0.0
121  assert captured['meter_type'] == 'ISO 5167 orifice'
122  assert captured['taps'] in ['corner', '25 millimeter']
123
124  with pytest.raises(ValueError, match='Name d not found'):
125    module_under_test.calcOrifice(Q=3, orifice='corner', Pin=2, Pout=1)
126
127def test_calc_orifice2_returns_diameter_from_newton_solution(monkeypatch) -> None:
128  class DummyCircuit:
129    rho = 1000 * u.kg / u.m**3
130    mu = 1e-3 * u.Pa * u.s
131    k = 1.4
132
133    @staticmethod
134    def calcH(_Q):
135      return 10 * u.m
136
137  monkeypatch.setattr(module_under_test.fu, 'P_from_head', lambda head, rho: 1.0 * u.bar)
138  monkeypatch.setattr(module_under_test, 'newton', lambda func, x0, tol: 0.5)
139
140  diameter = module_under_test.calcOrifice2(DummyCircuit(), 4 * u.m**3 / u.h, 80 * u.mm)
141
142  assert diameter.to(u.mm).magnitude == pytest.approx(40.0)

Tests: test_wpoint

  1'''Behavioral unit tests for fluidsolve.wpoint.'''
  2
  3# PYLINT DIRECTIVES
  4# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,unused-argument,no-member
  5
  6import inspect
  7import types
  8import pytest
  9import fluidsolve.wpoint as module_under_test
 10
 11u = module_under_test.u
 12
 13def test_module_importable() -> None:
 14  assert module_under_test is not None
 15
 16@pytest.mark.parametrize('name', ['Wpoint', 'WpointDyn'])
 17def test_public_classes_exist(name: str) -> None:
 18  obj = getattr(module_under_test, name)
 19  assert inspect.isclass(obj)
 20
 21@pytest.mark.parametrize('name', ['calcOperatingPoint'])
 22def test_public_functions_are_callable(name: str) -> None:
 23  obj = getattr(module_under_test, name)
 24  assert callable(obj)
 25
 26@pytest.mark.parametrize('name', ['Quantity', 'u'])
 27def test_public_variables_exist(name: str) -> None:
 28  assert hasattr(module_under_test, name)
 29
 30class DummyComp(module_under_test.flsb.Comp_Base):
 31  _group = 'Test'
 32  _part = 'Dummy'
 33
 34  def __init__(self, name: str='C', factor: float=1.0) -> None:
 35    super().__init__(name=name)
 36    self.factor = factor
 37    self.calls = []
 38
 39  def calcH(self, Q, sense: int=1, pin: int=1, pout: int=2):
 40    self.calls.append((Q, sense, pin, pout))
 41    q_mag = Q.to(u.m**3 / u.h).magnitude if hasattr(Q, 'to') else float(Q)
 42    return self.factor * q_mag * sense * u.m
 43
 44def test_wpoint_defaults_setters_and_magnitudes() -> None:
 45  point = module_under_test.Wpoint()
 46
 47  assert point.name == ''
 48  assert point.Qmag == 0.0
 49  assert point.Hmag == 0.0
 50
 51  point.name = 'WP1'
 52  point.Q = 3.5
 53  point.H = 8.0
 54  flow = point.Q
 55  head = point.H
 56
 57  assert point.name == 'WP1'
 58  assert getattr(flow, 'to')(u.m**3 / u.h).magnitude == pytest.approx(3.5)
 59  assert getattr(head, 'to')(u.m).magnitude == pytest.approx(8.0)
 60  assert point.Qmag == pytest.approx(3.5)
 61  assert point.Hmag == pytest.approx(8.0)
 62  assert point.update() is point
 63
 64def test_wpoint_string_and_repr_for_named_and_unnamed() -> None:
 65  unnamed = module_under_test.Wpoint(Q=2.0, H=4.0)
 66  named = module_under_test.Wpoint(name='NodeA', Q=2.0, H=4.0)
 67
 68  assert str(unnamed).startswith('Pt: Q:')
 69  assert repr(unnamed).startswith('Pt: Q:')
 70  assert f'{unnamed:1}' == str(unnamed)
 71  assert 'Pt NodeA:' in str(named)
 72  assert f'{named:1}' == str(named)
 73  assert 'Pt NodeA:' in repr(named)
 74
 75def test_calc_operating_point_returns_expected_values(monkeypatch) -> None:
 76  comp1 = DummyComp('C1', factor=-2.0)
 77  comp2 = DummyComp('C2', factor=3.0)
 78
 79  monkeypatch.setattr(
 80    module_under_test,
 81    'root',
 82    lambda func, x0, method='hybr': types.SimpleNamespace(success=True, x=[2.5], message='ok', status=1),
 83  )
 84
 85  q_op, h_op = module_under_test.calcOperatingPoint(comp1, comp2, guess=1.0)
 86
 87  assert q_op.to(u.m**3 / u.h).magnitude == pytest.approx(2.5)
 88  assert h_op.to(u.m).magnitude == pytest.approx(7.5)
 89  assert hasattr(comp2.calls[-1][0], 'to')
 90
 91def test_calc_operating_point_raises_when_solver_fails(monkeypatch) -> None:
 92  comp1 = DummyComp('C1', factor=-2.0)
 93  comp2 = DummyComp('C2', factor=3.0)
 94
 95  monkeypatch.setattr(
 96    module_under_test,
 97    'root',
 98    lambda func, x0, method='hybr': types.SimpleNamespace(success=False, x=[1.0], message='no convergence', status=4),
 99  )
100
101  with pytest.warns(RuntimeWarning, match='did not converge|no convergence'):
102    q_op, h_op = module_under_test.calcOperatingPoint(comp1, comp2)
103
104  assert q_op.to(u.m**3 / u.h).magnitude == 0.0
105  assert h_op.to(u.m).magnitude == 0.0
106
107def test_wpointdyn_uses_components_and_updates_from_operating_point(monkeypatch) -> None:
108  comp1 = DummyComp('C1', factor=-1.0)
109  comp2 = DummyComp('C2', factor=2.0)
110
111  monkeypatch.setattr(
112    module_under_test,
113    'calcOperatingPoint',
114    lambda s1, s2, guess: (4.0 * u.m**3 / u.h, 6.0 * u.m),
115  )
116
117  point = module_under_test.WpointDyn(name='dyn', s1=comp1, s2=comp2, guess=10)
118
119  assert point.name == 'dyn'
120  assert point.Qmag == pytest.approx(4.0)
121  assert point.Hmag == pytest.approx(6.0)
122
123  point.update()
124  assert point.Qmag == pytest.approx(4.0)
125  assert point.Hmag == pytest.approx(6.0)
126
127def test_wpointdyn_without_components_keeps_initial_values() -> None:
128  with pytest.raises(ValueError, match='argument s1 not of type'):
129    module_under_test.WpointDyn(name='dyn', Q=1.0, H=2.0)