syncmoss 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {syncmoss-0.4.2 → syncmoss-0.5.0}/PKG-INFO +1 -1
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/constants.py +4 -0
- syncmoss-0.5.0/syncmoss/exclusion_bar.py +252 -0
- syncmoss-0.5.0/syncmoss/exclusion_regions.py +137 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/fitting_io.py +202 -60
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/instrumental_io.py +439 -83
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/main.py +1 -1
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/minimi_lib.py +12 -5
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/model_io.py +57 -14
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/models.py +30 -11
- syncmoss-0.5.0/syncmoss/one_model.py +442 -0
- syncmoss-0.5.0/syncmoss/one_spectrum.py +109 -0
- syncmoss-0.5.0/syncmoss/parameters/Calibration.dat +513 -0
- syncmoss-0.5.0/syncmoss/parameters/GCMS.txt +1 -0
- syncmoss-0.5.0/syncmoss/parameters/INSexp.txt +1 -0
- syncmoss-0.5.0/syncmoss/parameters/INSint.txt +1 -0
- syncmoss-0.5.0/syncmoss/parameters/calibr.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/help.md +175 -24
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters_table.py +101 -21
- syncmoss-0.5.0/syncmoss/result_window.py +408 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/results_table.py +82 -92
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/spectrum_io.py +279 -30
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/spectrum_parameters.py +2 -2
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/spectrum_plotter.py +226 -107
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/support_math.py +17 -5
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/syncmoss_main.py +1139 -177
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/PKG-INFO +1 -1
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/SOURCES.txt +25 -1
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/scm_file_list.json +24 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/conftest.py +38 -17
- syncmoss-0.5.0/tests/data/parameters/INSth.txt +1 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/syncmoss_test.py +11 -3
- syncmoss-0.5.0/tests/test_create_spectrum_from_model.py +253 -0
- syncmoss-0.5.0/tests/test_exclusion_fit.py +264 -0
- syncmoss-0.5.0/tests/test_exclusion_gui.py +412 -0
- syncmoss-0.5.0/tests/test_exclusion_regions.py +148 -0
- syncmoss-0.5.0/tests/test_fit_core.py +286 -0
- syncmoss-0.5.0/tests/test_fit_one_spectrum.py +243 -0
- syncmoss-0.5.0/tests/test_instrumental_joint.py +305 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_instrumental_window.py +2 -7
- syncmoss-0.5.0/tests/test_minimi_bounds.py +68 -0
- syncmoss-0.5.0/tests/test_model_pointwise.py +120 -0
- syncmoss-0.5.0/tests/test_one_model_expand.py +262 -0
- syncmoss-0.5.0/tests/test_one_model_gui.py +516 -0
- syncmoss-0.5.0/tests/test_overwrite_dat_instrumental.py +282 -0
- syncmoss-0.5.0/tests/test_result_window.py +185 -0
- syncmoss-0.5.0/tests/test_results_table_rows.py +383 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_save_result_model.py +63 -10
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_search_guards.py +4 -2
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_spectrum_parameters.py +5 -5
- syncmoss-0.5.0/tests/test_support_math.py +166 -0
- syncmoss-0.4.2/syncmoss/parameters/calibr.png +0 -0
- syncmoss-0.4.2/tests/test_support_math.py +0 -87
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.gitattributes +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/Tests.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_MacOS.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_PyPI.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_TagRelease.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_Tests.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_UploadRelease.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_Version.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_Wheels.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/_WindowsExe.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.github/workflows/release.yml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.gitignore +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/.vscode/settings.json +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/COPYING.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/LICENSE +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/Lib_test/test.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/NOTICE.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/README.md +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/MacOS.spec +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/SYNCmoss.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/Windows.spec +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/bundle.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/dmg_background.pdf +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/licenses/GPL-3.0.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/licenses/LGPL-3.0.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/macos_bundle.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/bundle/third_party_licenses.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/docs/thick_models.md +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/docs/thick_vs_thin.md +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/pyproject.toml +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/requirements.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/setup.cfg +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Calibration.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Hamiltonian_helper.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Aegirine.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Akaganeite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Almandine.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Antitaenite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Arsenopyrite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Bornite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Bridgmanite (Fe,Al-bearing) @100 GPa.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Bridgmanite (Fe,Al-bearing).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Celadonite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Cementite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Chalcopyrite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Clinopyroxene (augite).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Copiapite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Cubanite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Epidote.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Fayalite (Fe-olivine).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Ferrihydrite (2-line).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Ferropericlase.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Goethite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Green rust (fougerite, carbonate).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Haegg carbide.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Hedenbergite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Hematite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Heterosite (FePO4).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Ilmenite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Iron phosphide.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Isocubanite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Jarosite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Kamacite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Lepidocrocite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Maghemite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Magnetite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Marcasite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Melanterite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Orthopyroxene (enstatite-ferrosilite).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Prussian blue (ferric ferrocyanide).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Pyrite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Pyrrhotite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Rhomboclase.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Roemerite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Siderite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Szomolnokite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Taenite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Talnakhite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Tetrataenite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Triphylite (LiFePO4).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Troilite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Vivianite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/Voltaite.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library/alpha-Fe (metallic iron, bcc).mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library_io.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/Library_window.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/bliss_channel.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/error_reporter.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/CheckBox.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/CheckBox_.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/CheckBox_L.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/CheckBox_L2.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/DU.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/Switch.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/UD.png +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/icons/icon_r.ico +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/instrumental_window.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/legacy.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/license_window.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/models_description_window.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/models_positions.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/ABSorber3.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/Be.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/INS_APS.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/INSth.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/KB.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/NFS.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/parameters/models_description.md +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/sms_theory.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/supp_menu.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/test_graf.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/test_param.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/theme_dark.json +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss/theme_light.json +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/dependency_links.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/entry_points.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/requires.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/syncmoss.egg-info/top_level.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/data/Distr_corr.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/data/Distr_distr.mdl +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/data/alpha_fe_sms_000.mca +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/data/calibration_000_golden.json +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/data/generate_cal_fixtures.py +0 -0
- {syncmoss-0.4.2/syncmoss → syncmoss-0.5.0/tests/data}/parameters/Calibration.dat +0 -0
- {syncmoss-0.4.2/syncmoss → syncmoss-0.5.0/tests/data}/parameters/GCMS.txt +0 -0
- {syncmoss-0.4.2/syncmoss → syncmoss-0.5.0/tests/data}/parameters/INSexp.txt +0 -0
- {syncmoss-0.4.2/syncmoss → syncmoss-0.5.0/tests/data}/parameters/INSint.txt +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/data/synthetic_alpha_fe_cms_linear.mca +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/golden_models.json +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_bliss_channel.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_calibration.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_calibration_helpers.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_calibration_path.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_chi2_spread.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_contact_author.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_distr_composition.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_distr_expansion.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_distr_mdl.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_distr_targets.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_error_reporter.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_expression_fit_start.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_expression_validation.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_golden_models.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_gui_smoke.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_hamiltonian_texture.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_impurity_presets.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_instrumental_metadata.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_instrumental_method_choice.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_instrumental_recentre.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_integral_points.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_interrupt.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_legacy.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_library_dir.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_library_io.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_license_window.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_model_append.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_model_io.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_model_only_mode.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_nbaseline_results.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_parameter_links.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_polarization.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_recon_composition.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_recon_mdl.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_recon_results.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_reference_shift.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_results_table_image.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_sdw_grid.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_sms_theory.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_table_marks.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_take_result.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_texture_s1.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_theory_search.py +0 -0
- {syncmoss-0.4.2 → syncmoss-0.5.0}/tests/test_thick_gui.py +0 -0
|
@@ -92,6 +92,10 @@ number_of_baseline_parameters = 8
|
|
|
92
92
|
# Default color sequence for plotting models
|
|
93
93
|
model_colors = ['red', 'blue', 'cyan', 'yellow', 'fuchsia', 'lime', 'darkorange', 'blueviolet', 'green', 'tomato', 'white', 'silver', 'lightgreen', 'pink']
|
|
94
94
|
|
|
95
|
+
# An Nbaseline row is shown light grey: it opens the next spectrum's section,
|
|
96
|
+
# it is not a component. 'silver' is a colour every .mdl reader already knows.
|
|
97
|
+
NBASELINE_COLOR = 'silver'
|
|
98
|
+
|
|
95
99
|
# Every color name a .mdl color row may contain (used to recognise the color
|
|
96
100
|
# line when loading model files — shared by model_io and Library_io so both
|
|
97
101
|
# accept the same files).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""The exclusion-regions bar of the main window, between the plot toolbar and
|
|
2
|
+
the plot: the regions as text, "Pick on plot", Save and Load.
|
|
3
|
+
|
|
4
|
+
It is shown while "Apply exclusion" (the plot toolbar) is on. ``regions`` are
|
|
5
|
+
the applied regions; they change only through commit(), which emits
|
|
6
|
+
``regions_changed``. What a change does to the plot and to a fit result is the
|
|
7
|
+
main window's business (PhysicsApp._on_exclusion_changed).
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from PySide6.QtCore import Qt, QEvent, Signal
|
|
13
|
+
from PySide6.QtGui import QFont, QKeySequence, QShortcut
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QSizePolicy,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from syncmoss import exclusion_regions as er
|
|
19
|
+
from syncmoss.spectrum_plotter import (
|
|
20
|
+
DATA_GID, SPECTRUM_AXES_GID, EXCLUSION_COLOR, spectrum_axes_of,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_FILE_FILTER = "Exclusion regions (*_exclusion.txt);;Text files (*.txt);;All files (*)"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExclusionBar(QWidget):
|
|
27
|
+
"""The text box of the exclusion regions and its buttons."""
|
|
28
|
+
|
|
29
|
+
regions_changed = Signal()
|
|
30
|
+
|
|
31
|
+
def __init__(self, main_window):
|
|
32
|
+
super().__init__(main_window)
|
|
33
|
+
self.main_window = main_window
|
|
34
|
+
self.regions = () # the applied regions
|
|
35
|
+
self._first = None # the first end of a region being picked
|
|
36
|
+
self._marks = [] # its dashed lines on the plot
|
|
37
|
+
|
|
38
|
+
# Its own height only: the plot takes every spare pixel
|
|
39
|
+
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
|
40
|
+
font = QFont('Arial', 14)
|
|
41
|
+
|
|
42
|
+
label = QLabel("Exclusion regions, mm/s:")
|
|
43
|
+
label.setFont(font)
|
|
44
|
+
self.text = QLineEdit()
|
|
45
|
+
self.text.setFont(font)
|
|
46
|
+
self.text.setPlaceholderText("-3:-2; 1:2")
|
|
47
|
+
self.text.setToolTip("Velocity regions whose points are not fitted: lo:hi; lo:hi\n"
|
|
48
|
+
"Enter or leaving the box applies them.")
|
|
49
|
+
self.text.installEventFilter(self)
|
|
50
|
+
self.text.editingFinished.connect(self.commit)
|
|
51
|
+
self.text.textEdited.connect(lambda _text: self._mark_invalid(False))
|
|
52
|
+
|
|
53
|
+
self.pick_btn = QPushButton("Pick on plot")
|
|
54
|
+
self.pick_btn.setFont(font)
|
|
55
|
+
self.pick_btn.setCheckable(True)
|
|
56
|
+
self.pick_btn.setToolTip("Click the two ends of a region on the plot (Esc cancels)")
|
|
57
|
+
self.pick_btn.toggled.connect(self._pick_toggled)
|
|
58
|
+
self.clean_btn = QPushButton("Clean")
|
|
59
|
+
self.clean_btn.setFont(font)
|
|
60
|
+
self.clean_btn.setToolTip("Remove every exclusion region")
|
|
61
|
+
self.clean_btn.clicked.connect(self.clean)
|
|
62
|
+
self.save_btn = QPushButton("Save")
|
|
63
|
+
self.save_btn.setFont(font)
|
|
64
|
+
self.save_btn.clicked.connect(self.save)
|
|
65
|
+
self.load_btn = QPushButton("Load")
|
|
66
|
+
self.load_btn.setFont(font)
|
|
67
|
+
self.load_btn.setToolTip("Replace the regions with those of a file")
|
|
68
|
+
self.load_btn.clicked.connect(self.load)
|
|
69
|
+
|
|
70
|
+
# The label and the buttons on one line, the regions under them
|
|
71
|
+
buttons = QHBoxLayout()
|
|
72
|
+
buttons.addWidget(label)
|
|
73
|
+
buttons.addStretch(1)
|
|
74
|
+
buttons.addWidget(self.pick_btn)
|
|
75
|
+
buttons.addWidget(self.clean_btn)
|
|
76
|
+
buttons.addWidget(self.save_btn)
|
|
77
|
+
buttons.addWidget(self.load_btn)
|
|
78
|
+
layout = QVBoxLayout(self)
|
|
79
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
80
|
+
layout.addLayout(buttons)
|
|
81
|
+
layout.addWidget(self.text)
|
|
82
|
+
|
|
83
|
+
# Esc cancels a pick; enabled only while picking
|
|
84
|
+
self.escape = QShortcut(QKeySequence(Qt.Key.Key_Escape), main_window)
|
|
85
|
+
self.escape.setEnabled(False)
|
|
86
|
+
self.escape.activated.connect(self.cancel_pick)
|
|
87
|
+
|
|
88
|
+
# --- the text ---------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def eventFilter(self, obj, event):
|
|
91
|
+
# Enter in the box applies the regions: it must not reach the window's
|
|
92
|
+
# Return shortcut (Show model)
|
|
93
|
+
if (obj is self.text and event.type() == QEvent.Type.ShortcutOverride
|
|
94
|
+
and event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter)):
|
|
95
|
+
event.accept()
|
|
96
|
+
return True
|
|
97
|
+
return super().eventFilter(obj, event)
|
|
98
|
+
|
|
99
|
+
def _mark_invalid(self, invalid):
|
|
100
|
+
self.text.setStyleSheet("background-color: red; color: white;" if invalid else "")
|
|
101
|
+
|
|
102
|
+
def commit(self, action_label=None):
|
|
103
|
+
"""Apply the text. True when it holds regions -- written back cleaned,
|
|
104
|
+
and ``regions_changed`` emitted when they differ from the applied ones.
|
|
105
|
+
False (and the reason in the status) when it does not, or when they
|
|
106
|
+
cannot change now; then the applied regions stay. *action_label* names
|
|
107
|
+
what is not started because of it."""
|
|
108
|
+
try:
|
|
109
|
+
regions = er.parse(self.text.text())
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
self._mark_invalid(True)
|
|
112
|
+
if action_label:
|
|
113
|
+
self.main_window.set_status(
|
|
114
|
+
f"{action_label} was not started — exclusion regions: {e}", "red")
|
|
115
|
+
else:
|
|
116
|
+
self.main_window.set_status(f"Exclusion regions: {e}", "red")
|
|
117
|
+
return False
|
|
118
|
+
self._mark_invalid(False)
|
|
119
|
+
if regions != self.regions and self.main_window._reject_if_busy('Changing exclusion regions'):
|
|
120
|
+
self.text.setText(er.format_regions(self.regions))
|
|
121
|
+
return False
|
|
122
|
+
self.text.setText(er.format_regions(regions))
|
|
123
|
+
if regions != self.regions:
|
|
124
|
+
self.regions = regions
|
|
125
|
+
self.regions_changed.emit()
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def clean(self):
|
|
129
|
+
"""Remove every region ("Apply exclusion" stays as it is); refused,
|
|
130
|
+
like any change, while a calculation runs."""
|
|
131
|
+
self.pick_btn.setChecked(False)
|
|
132
|
+
self.text.setText("")
|
|
133
|
+
self.commit()
|
|
134
|
+
|
|
135
|
+
# --- Pick on plot -----------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _pick_toggled(self, on):
|
|
138
|
+
if not on:
|
|
139
|
+
self._forget_first()
|
|
140
|
+
self.escape.setEnabled(False)
|
|
141
|
+
return
|
|
142
|
+
if self.main_window._reject_if_busy('Picking an exclusion region'):
|
|
143
|
+
self.pick_btn.setChecked(False)
|
|
144
|
+
return
|
|
145
|
+
self.escape.setEnabled(True)
|
|
146
|
+
self.main_window.set_status("Pick on plot: click the two ends of the region "
|
|
147
|
+
"(Esc cancels)", "blue")
|
|
148
|
+
|
|
149
|
+
def cancel_pick(self):
|
|
150
|
+
if self.pick_btn.isChecked():
|
|
151
|
+
self.pick_btn.setChecked(False)
|
|
152
|
+
self.main_window.set_status("Picking canceled", "orange")
|
|
153
|
+
|
|
154
|
+
def _forget_first(self):
|
|
155
|
+
self._first = None
|
|
156
|
+
for mark in self._marks:
|
|
157
|
+
mark.remove()
|
|
158
|
+
if self._marks:
|
|
159
|
+
self.main_window.canvas.draw_idle()
|
|
160
|
+
self._marks = []
|
|
161
|
+
|
|
162
|
+
def on_canvas_click(self, event):
|
|
163
|
+
"""A click on the main plot (connected to the canvas, so it survives
|
|
164
|
+
every redraw): one end of the region being picked."""
|
|
165
|
+
if not self.pick_btn.isChecked() or event.button != 1:
|
|
166
|
+
return
|
|
167
|
+
if str(self.main_window.toolbar.mode): # Pan or Zoom is on
|
|
168
|
+
return
|
|
169
|
+
ax = event.inaxes
|
|
170
|
+
if ax is None or ax.get_gid() != SPECTRUM_AXES_GID or event.xdata is None:
|
|
171
|
+
return
|
|
172
|
+
end = self._boundary(ax, float(event.xdata))
|
|
173
|
+
if self._first is None:
|
|
174
|
+
self._first = end
|
|
175
|
+
for spectrum_ax in spectrum_axes_of(ax.figure):
|
|
176
|
+
xlim = spectrum_ax.get_xlim()
|
|
177
|
+
self._marks.append(spectrum_ax.axvline(end, color=EXCLUSION_COLOR,
|
|
178
|
+
linestyle='--', linewidth=1))
|
|
179
|
+
spectrum_ax.set_xlim(xlim)
|
|
180
|
+
self.main_window.canvas.draw_idle()
|
|
181
|
+
self.main_window.set_status(f"One end at {er.format_velocity(end)} mm/s: "
|
|
182
|
+
f"click the other end (Esc cancels)", "blue")
|
|
183
|
+
return
|
|
184
|
+
first = self._first
|
|
185
|
+
self.pick_btn.setChecked(False) # forgets the first end
|
|
186
|
+
if first == end:
|
|
187
|
+
self.main_window.set_status("Both ends are between the same two points: "
|
|
188
|
+
"no point would be excluded", "orange")
|
|
189
|
+
return
|
|
190
|
+
try:
|
|
191
|
+
current = er.parse(self.text.text())
|
|
192
|
+
except ValueError:
|
|
193
|
+
current = self.regions
|
|
194
|
+
self.text.setText(er.format_regions(er.normalize(current + ((first, end),))))
|
|
195
|
+
self.commit()
|
|
196
|
+
|
|
197
|
+
def _boundary(self, ax, x):
|
|
198
|
+
"""Where an end clicked at *x* goes: between the data points around it,
|
|
199
|
+
or, on a plot without data points, within half a pixel."""
|
|
200
|
+
grid = [np.asarray(line.get_xdata(), dtype=float)
|
|
201
|
+
for line in ax.lines if line.get_gid() == DATA_GID]
|
|
202
|
+
if grid:
|
|
203
|
+
end = er.boundary_between_points(np.concatenate(grid), x)
|
|
204
|
+
if end is not None:
|
|
205
|
+
return end
|
|
206
|
+
x0, x1 = ax.get_xlim()
|
|
207
|
+
return er.boundary_at_pixel(x, abs(x1 - x0) / max(ax.bbox.width, 1.0) / 2)
|
|
208
|
+
|
|
209
|
+
# --- Save / Load ------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def save(self):
|
|
212
|
+
if not self.commit():
|
|
213
|
+
return
|
|
214
|
+
if not self.regions:
|
|
215
|
+
self.main_window.set_status("No exclusion regions to save", "orange")
|
|
216
|
+
return
|
|
217
|
+
base = self.main_window._result_base_path()
|
|
218
|
+
default = (base + er.FILE_SUFFIX if base
|
|
219
|
+
else os.path.join(self.main_window.workfolder or "", "regions" + er.FILE_SUFFIX))
|
|
220
|
+
path, _ = QFileDialog.getSaveFileName(self, "Save exclusion regions", default, _FILE_FILTER)
|
|
221
|
+
if not path:
|
|
222
|
+
self.main_window.set_status("Saving canceled", "orange")
|
|
223
|
+
return
|
|
224
|
+
try:
|
|
225
|
+
er.write_file(path, self.regions)
|
|
226
|
+
except OSError as e:
|
|
227
|
+
self.main_window.set_status(f"Exclusion regions not saved: {e}", "red")
|
|
228
|
+
return
|
|
229
|
+
self.main_window.set_status(f"Exclusion regions saved to {os.path.basename(path)}", "green")
|
|
230
|
+
|
|
231
|
+
def load(self):
|
|
232
|
+
"""Replace the regions with those of a file."""
|
|
233
|
+
path, _ = QFileDialog.getOpenFileName(self, "Load exclusion regions",
|
|
234
|
+
self.main_window.workfolder or "", _FILE_FILTER)
|
|
235
|
+
if not path:
|
|
236
|
+
self.main_window.set_status("Loading canceled", "orange")
|
|
237
|
+
return
|
|
238
|
+
self.load_file(path)
|
|
239
|
+
|
|
240
|
+
def load_file(self, path):
|
|
241
|
+
try:
|
|
242
|
+
regions = er.read_file(path)
|
|
243
|
+
except (OSError, ValueError) as e:
|
|
244
|
+
self.main_window.set_status(f"Exclusion regions not loaded from "
|
|
245
|
+
f"{os.path.basename(path)}: {e}", "red")
|
|
246
|
+
return False
|
|
247
|
+
self.text.setText(er.format_regions(regions))
|
|
248
|
+
if not self.commit():
|
|
249
|
+
return False
|
|
250
|
+
self.main_window.set_status(f"Exclusion regions loaded from {os.path.basename(path)}: "
|
|
251
|
+
f"{er.format_regions(regions) or 'none'}", "green")
|
|
252
|
+
return True
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Exclusion regions: velocity ranges whose points are left out of a fit.
|
|
2
|
+
|
|
3
|
+
A region is a pair ``(lo, hi)`` in mm/s. The regions of a fit are a tuple of
|
|
4
|
+
such pairs, sorted, each with ``lo < hi``, none overlapping or touching; ``()``
|
|
5
|
+
means none. A point is excluded when ``lo <= v <= hi``.
|
|
6
|
+
|
|
7
|
+
The model at one velocity does not depend on the other velocities computed --
|
|
8
|
+
the instrumental function is integrated over the source energy for every
|
|
9
|
+
velocity on its own (tests/test_model_pointwise.py) -- so a fit with exclusion
|
|
10
|
+
regions is simply a fit of the points that are left.
|
|
11
|
+
|
|
12
|
+
The user types the regions as ``lo:hi; lo:hi``; only ';' separates them.
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
FILE_SUFFIX = '_exclusion.txt'
|
|
19
|
+
FILE_HEADER = '# SYNCmoss exclusion regions, velocity in mm/s'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def normalize(pairs):
|
|
23
|
+
"""The regions of *pairs* (each in either order): sorted, overlapping or
|
|
24
|
+
touching ones merged."""
|
|
25
|
+
ordered = sorted((min(float(a), float(b)), max(float(a), float(b))) for a, b in pairs)
|
|
26
|
+
merged = []
|
|
27
|
+
for lo, hi in ordered:
|
|
28
|
+
if merged and lo <= merged[-1][1]:
|
|
29
|
+
merged[-1] = (merged[-1][0], max(merged[-1][1], hi))
|
|
30
|
+
else:
|
|
31
|
+
merged.append((lo, hi))
|
|
32
|
+
return tuple(merged)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse(text):
|
|
36
|
+
"""The regions written in *text* (``lo:hi; lo:hi``), normalized.
|
|
37
|
+
|
|
38
|
+
Raises ValueError with a message for the user. A ',' anywhere is refused,
|
|
39
|
+
so a decimal comma can never be read as something else.
|
|
40
|
+
"""
|
|
41
|
+
text = str(text).strip()
|
|
42
|
+
if ',' in text:
|
|
43
|
+
raise ValueError("use ';' between regions and '.' for decimals")
|
|
44
|
+
pairs = []
|
|
45
|
+
for piece in text.split(';'):
|
|
46
|
+
piece = piece.strip()
|
|
47
|
+
if not piece:
|
|
48
|
+
continue
|
|
49
|
+
ends = piece.split(':')
|
|
50
|
+
if len(ends) != 2:
|
|
51
|
+
raise ValueError(f"'{piece}' is not a region: write it as lo:hi")
|
|
52
|
+
try:
|
|
53
|
+
lo, hi = float(ends[0]), float(ends[1])
|
|
54
|
+
except ValueError:
|
|
55
|
+
raise ValueError(f"'{piece}': both ends must be numbers") from None
|
|
56
|
+
if not (np.isfinite(lo) and np.isfinite(hi)):
|
|
57
|
+
raise ValueError(f"'{piece}': both ends must be finite numbers")
|
|
58
|
+
if lo == hi:
|
|
59
|
+
raise ValueError(f"'{piece}' is empty: its two ends are the same")
|
|
60
|
+
pairs.append((lo, hi))
|
|
61
|
+
return normalize(pairs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def format_velocity(value):
|
|
65
|
+
"""*value* written in the shortest form that reads back exactly."""
|
|
66
|
+
return np.format_float_positional(float(value), trim='-')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_regions(regions):
|
|
70
|
+
"""``-3.05:-1.95; 0.95:2.05`` -- the text parse() reads back."""
|
|
71
|
+
return '; '.join(f'{format_velocity(lo)}:{format_velocity(hi)}' for lo, hi in regions)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def kept(velocities, regions):
|
|
75
|
+
"""Boolean mask over *velocities*: True for the points that are fitted."""
|
|
76
|
+
v = np.asarray(velocities, dtype=float)
|
|
77
|
+
keep = np.ones(v.shape, dtype=bool)
|
|
78
|
+
for lo, hi in regions:
|
|
79
|
+
keep &= ~((v >= lo) & (v <= hi))
|
|
80
|
+
return keep
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def shortest_decimal_in(lo, hi, target):
|
|
84
|
+
"""The number with the fewest decimals strictly between *lo* and *hi*;
|
|
85
|
+
of those, the one closest to *target*."""
|
|
86
|
+
lo, hi = min(lo, hi), max(lo, hi)
|
|
87
|
+
for decimals in range(sys.float_info.dig + 3):
|
|
88
|
+
scale = 10.0 ** decimals
|
|
89
|
+
first, last = int(np.floor(lo * scale)), int(np.ceil(hi * scale))
|
|
90
|
+
nearest = int(min(max(round(target * scale), first), last))
|
|
91
|
+
# the products above are rounded, so the candidates next to the
|
|
92
|
+
# nearest one are checked as well
|
|
93
|
+
for k in sorted((nearest - 1, nearest, nearest + 1),
|
|
94
|
+
key=lambda k: abs(k - target * scale)):
|
|
95
|
+
if lo < k / scale < hi:
|
|
96
|
+
return k / scale
|
|
97
|
+
return float(target)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def boundary_between_points(grid, x):
|
|
101
|
+
"""Where a region boundary clicked at *x* goes: between the two points of
|
|
102
|
+
*grid* around *x* (beyond an end, between the end point and one step
|
|
103
|
+
further), written with the fewest decimals that tell those points apart.
|
|
104
|
+
None when the grid has fewer than two points."""
|
|
105
|
+
points = np.unique(np.asarray(grid, dtype=float))
|
|
106
|
+
if points.size < 2:
|
|
107
|
+
return None
|
|
108
|
+
i = int(np.searchsorted(points, x))
|
|
109
|
+
if i == 0:
|
|
110
|
+
left, right = 2 * points[0] - points[1], points[0]
|
|
111
|
+
elif i == points.size:
|
|
112
|
+
left, right = points[-1], 2 * points[-1] - points[-2]
|
|
113
|
+
else:
|
|
114
|
+
left, right = points[i - 1], points[i]
|
|
115
|
+
return shortest_decimal_in(left, right, x)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def boundary_at_pixel(x, half_pixel):
|
|
119
|
+
"""A boundary clicked at *x* on a plot without data points: the fewest
|
|
120
|
+
decimals within half a pixel (*half_pixel*, in mm/s) of the click."""
|
|
121
|
+
return shortest_decimal_in(x - half_pixel, x + half_pixel, x)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def write_file(path, regions):
|
|
125
|
+
"""Write *regions* to an exclusion-regions file (read_file reads it)."""
|
|
126
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
127
|
+
f.write(FILE_HEADER + '\n')
|
|
128
|
+
f.write(format_regions(regions) + '\n')
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def read_file(path):
|
|
132
|
+
"""The regions of an exclusion-regions file: its lines that are not
|
|
133
|
+
comments, joined with ';'. Raises ValueError for a bad file (OSError when
|
|
134
|
+
it cannot be read)."""
|
|
135
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
136
|
+
lines = [line.strip() for line in f]
|
|
137
|
+
return parse('; '.join(line for line in lines if line and not line.startswith('#')))
|