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)