nwp500-python 9.8.0__tar.gz → 9.9.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.
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/CHANGELOG.rst +46 -0
- {nwp500_python-9.8.0/src/nwp500_python.egg-info → nwp500_python-9.9.0}/PKG-INFO +1 -1
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/simulator.rst +5 -1
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/calibrate-simulator.rst +19 -8
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/sim.rst +3 -1
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/api.py +1 -5
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/calibration.py +366 -22
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/decay.py +29 -1
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/params.py +17 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/physics/compressor.py +12 -2
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/physics/tank.py +68 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/tank_physics.py +2 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim_calibrate.py +171 -128
- {nwp500_python-9.8.0 → nwp500_python-9.9.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/SOURCES.txt +1 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/scm_file_list.json +1 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/scm_version.json +2 -2
- nwp500_python-9.9.0/tests/test_sim_bottom_loss.py +665 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_calibrate.py +20 -12
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.coveragerc +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.gitignore +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/LICENSE.txt +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/Makefile +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/README.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/RELEASE.md +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/Makefile +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/conf.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/draws.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/heating-elements.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/recovery-speed.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/standby-loss.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/bluetooth.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/simulate-heater.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/index.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/reference/simulator-inputs.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/README.md +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/mask.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/simulator/lower-setpoint.json +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/simulator/plan.json +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/simulator/scenario.json +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/pyproject.toml +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/README.md +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/format.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/lint.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/setup.cfg +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/setup.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/ble/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/ble/__main__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/ble/client.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/ble/protocol.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/ble_backend.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/calibration.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/recording.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/simulate.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/account.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/maintenance.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/attribution.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/book.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/circulation.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/codec.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/cold_supply.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/controller.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/defaults.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/delivery.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/draw_forecast.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/engine.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/extensions.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/heating_trajectory.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/inputs.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/location_air.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/meter.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/observed.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/physics/__init__.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/physics/draw.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/physics/element.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/profile.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/ready.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/record.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/result.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/schedules.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/tou_triggers.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim/units.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim_adapters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim_home_assistant.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/sim_live.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/status_history.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/_sim_site.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/conftest.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/fixtures/sim/engine_golden.json +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/fixtures/sim/heater_params.json +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_api_endpoints_201.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_auth_session_refresh.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_ble_mqtt_adapter.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_ble_protocol.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_cli_calibrate.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_cli_simulate.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_events.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_models.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_control_fail.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_last_will.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_navilink_201_enums.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_reservation_validation.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_adapters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_api.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_book.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_book_history.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_calibration.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_change_history.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_circulation.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_codec.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_cold_supply.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_controller.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_defaults.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_draw_forecast.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_draw_loss.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_element.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_element_fit.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_forecast.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_golden.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_home_assistant.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_hot_water.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_import_contract.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_live.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_location_air.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_meter.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_no_clock_no_state.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_observed.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_profile.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_readiness.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_readiness_fit.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_ready.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_ready_api.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_replay.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_schedules.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_sim_tank_physics.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_status_history.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/tox.ini +0 -0
- {nwp500_python-9.8.0 → nwp500_python-9.9.0}/uv.lock +0 -0
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 9.9.0 (2026-10-22)
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Added
|
|
9
|
+
-----
|
|
10
|
+
- **A bottom loss in the standby model** (#187). Over one installation's 407
|
|
11
|
+
quiet windows across ten months, a window that opens stratified loses
|
|
12
|
+
heat at the bottom: its gap widens, its lower probe falls about 40 %
|
|
13
|
+
faster than a mixed tank's, and its upper probe hardly moves. One UA
|
|
14
|
+
on the whole tank could not represent it, and the fitted UA was a
|
|
15
|
+
blend that cooled a mixed tank too fast, as after a cold season's full
|
|
16
|
+
recoveries. ``LossParams`` gains ``bottom_loss_btu_hr`` and
|
|
17
|
+
``bottom_gate_f``: heat leaving the lower zone, of which the lower
|
|
18
|
+
probe sees ``1 - exp(-gap / gate)`` (``sim.physics.tank.
|
|
19
|
+
bottom_loss_reach``). The standby walk (``decay.project_decay``) and
|
|
20
|
+
the recovery integrator charge it to the lower zone. A bottom loss of
|
|
21
|
+
0.0, the default and what stored parameter sets carry, reproduces the
|
|
22
|
+
model before it.
|
|
23
|
+
- ``sim.calibration.fit_loss`` fits the UA law and the bottom loss
|
|
24
|
+
together, through the standby walk, on the quiet windows opening with
|
|
25
|
+
a gap of 10 °F or less (``loss_windows``); ``fit_standby`` uses it.
|
|
26
|
+
On the installation above, the lower probe's median error over ten
|
|
27
|
+
hours fell from 1.25 to 0.77 °F from March to October and from 1.67
|
|
28
|
+
to 1.05 °F over the winter (1.15 °F trained without the winter).
|
|
29
|
+
``StandbyWindow`` carries each window's probes and air, and
|
|
30
|
+
``standby_ends`` and ``standby_error`` walk a loss over windows.
|
|
31
|
+
- The report has the bottom loss and its gate, and ``keep_better``
|
|
32
|
+
judges them with the standby loss, on the probes' error in each
|
|
33
|
+
held-out window's fall, °F/h.
|
|
34
|
+
|
|
35
|
+
Changed
|
|
36
|
+
-------
|
|
37
|
+
- The standby loss is fitted only on quiet windows opening with a gap of
|
|
38
|
+
10 °F or less: a wider gap is a draw's front closing, which the gap's
|
|
39
|
+
time constant describes.
|
|
40
|
+
- ``keep_better`` scores the standby group on the probes' error in each
|
|
41
|
+
held-out window's fall, walked as the simulation walks it, in place of
|
|
42
|
+
the error in log UA, so a loss with a bottom term and one without are
|
|
43
|
+
scored alike.
|
|
44
|
+
- The zone gap time constant is fitted with the bottom loss opening the
|
|
45
|
+
gaps it relaxes (``fit_gap_tau``'s ``loss``, ``gap_paths``), in turn
|
|
46
|
+
with the loss until it settles: a gap the bottom loss widens read as a
|
|
47
|
+
pure relaxation pushed the constant long. ``keep_better`` judges the
|
|
48
|
+
constant before the standby loss and fits and scores the loss on the
|
|
49
|
+
constant kept.
|
|
50
|
+
|
|
5
51
|
Version 9.8.0 (2026-10-21)
|
|
6
52
|
==========================
|
|
7
53
|
|
|
@@ -78,7 +78,11 @@ zone (about half the tank's mass). Once the gap between the zones
|
|
|
78
78
|
closes, the tank heats as one body. In standby the gap relaxes on a
|
|
79
79
|
fitted time constant. Standby loss follows a jacket law,
|
|
80
80
|
``UA(dT) = ua_ref * (max(dT, 5) / 50) ^ n``, with the ambient as the
|
|
81
|
-
other side.
|
|
81
|
+
other side, on the whole tank. A bottom loss ``Q_b`` leaves the lower
|
|
82
|
+
zone alone: the lower probe sees the share ``1 - exp(-gap / gate)`` of
|
|
83
|
+
it, so a stratified tank's lower zone falls faster and its gap opens,
|
|
84
|
+
while a mixed tank hides it below the probe. With ``Q_b = 0`` the loss
|
|
85
|
+
is the jacket law alone.
|
|
82
86
|
|
|
83
87
|
**The elements.** They follow the device's own rules
|
|
84
88
|
(:doc:`heating-elements`): per-mode entry thresholds, a dead time,
|
|
@@ -139,16 +139,23 @@ fitted or kept the base's value, and why:
|
|
|
139
139
|
|
|
140
140
|
* - Term
|
|
141
141
|
- Fitted from
|
|
142
|
-
* - Standby loss and its
|
|
142
|
+
* - Standby loss, its exponent, the bottom loss and its gate
|
|
143
143
|
- Quiet windows of 3 h or more, starting 45 minutes after any
|
|
144
|
-
heating or hot water in use
|
|
145
|
-
|
|
146
|
-
the
|
|
147
|
-
|
|
144
|
+
heating or hot water in use, that open with a gap of 10 °F or
|
|
145
|
+
less between the probes. A window counts only if its fit tracks
|
|
146
|
+
the probes within 2 °F. Each window's upper and lower probe are
|
|
147
|
+
walked from its start as the simulation walks them, and the
|
|
148
|
+
terms are fitted to where the probes ended. The exponent is
|
|
149
|
+
fitted only when the windows span two 20 °F bands of tank-to-air
|
|
150
|
+
difference; it is held at 0 (a constant loss) otherwise. The
|
|
151
|
+
bottom loss needs 10 windows that open with a gap of 1 °F or
|
|
152
|
+
more; with fewer it is 0, and the loss is the jacket law
|
|
153
|
+
alone.
|
|
148
154
|
* - Zone gap time constant
|
|
149
155
|
- How fast the upper and lower probes' gap closes over quiet
|
|
150
156
|
windows; it needs 20 windows that open with a gap of 2 °F or
|
|
151
|
-
more.
|
|
157
|
+
more. The bottom loss opens the gaps it relaxes, so the two are
|
|
158
|
+
fitted in turn until the constant settles.
|
|
152
159
|
* - Compressor constant, evaporator approach
|
|
153
160
|
- Heat-pump recoveries that reached their target, against the loss
|
|
154
161
|
just fitted. The error is the mean absolute error of the
|
|
@@ -240,8 +247,12 @@ fit as ``params`` with ``keep_better=True``:
|
|
|
240
247
|
|
|
241
248
|
Each group of terms holds out its latest evidence from its refit:
|
|
242
249
|
|
|
243
|
-
- the standby loss and its
|
|
244
|
-
|
|
250
|
+
- the standby loss, its exponent, the bottom loss and its gate: its
|
|
251
|
+
quiet windows, scored on the probes' error in each window's fall,
|
|
252
|
+
°F/h;
|
|
253
|
+
- the zone gap time constant: its windows that open a gap, judged
|
|
254
|
+
before the standby loss, which is then fitted and scored on the
|
|
255
|
+
constant kept;
|
|
245
256
|
- the compressor constant and the evaporator approach: its reached
|
|
246
257
|
recoveries;
|
|
247
258
|
- the power scale: the metered compressor cycles it can use;
|
|
@@ -169,6 +169,8 @@ Hot water and the heat book
|
|
|
169
169
|
metered_withdrawn, metered_lead,
|
|
170
170
|
path_to_end, with_power, snapshots, recoveries, quiet_windows,
|
|
171
171
|
busy_intervals, fit_window, fit_ua_law, fit_gap_tau, with_standby,
|
|
172
|
+
fit_loss, LossFit, loss_windows, standby_ends, standby_error,
|
|
173
|
+
walk_tau, gap_paths, gap_error,
|
|
172
174
|
with_compressor, UA_SEARCH_BOUNDS, UA_EXPONENT_BOUNDS, DRAW_LEAD
|
|
173
175
|
|
|
174
176
|
.. automodule:: nwp500.sim.profile
|
|
@@ -250,7 +252,7 @@ The pieces the entry points are built from.
|
|
|
250
252
|
vapor_enthalpy, dew_point_f
|
|
251
253
|
|
|
252
254
|
.. automodule:: nwp500.sim.physics.tank
|
|
253
|
-
:members: ua_at
|
|
255
|
+
:members: ua_at, bottom_loss_reach, bottom_loss_drop_f
|
|
254
256
|
|
|
255
257
|
.. automodule:: nwp500.sim.physics.element
|
|
256
258
|
:members: mode_recovery, ModeRecovery, mode_plan, ModePlan,
|
|
@@ -78,7 +78,7 @@ from nwp500.sim.inputs import (
|
|
|
78
78
|
Schedules,
|
|
79
79
|
Settings,
|
|
80
80
|
)
|
|
81
|
-
from nwp500.sim.params import HeaterParams
|
|
81
|
+
from nwp500.sim.params import TAU_GAP_PRIOR_H, HeaterParams
|
|
82
82
|
from nwp500.sim.physics.compressor import MAX_MINUTES, predict_physics_detailed
|
|
83
83
|
from nwp500.sim.physics.element import (
|
|
84
84
|
DEVICE_MODE,
|
|
@@ -224,10 +224,6 @@ LOW_CONFIDENCE_FLOOR = 0.6
|
|
|
224
224
|
#: start was never searched (the TOU-opening start still was), so an
|
|
225
225
|
#: answer may miss a start the heater makes.
|
|
226
226
|
HP_LOWER_ON_UNKNOWN = "hp_lower_on_unknown"
|
|
227
|
-
#: The gap time constant served when the parameters carry none
|
|
228
|
-
#: (``TankParams.stratification_gap_tau_h``), h: history puts it near
|
|
229
|
-
#: 60 h on this product.
|
|
230
|
-
TAU_GAP_PRIOR_H = 60.0
|
|
231
227
|
#: Draws opened a gap that closes on that prior, not a fitted constant:
|
|
232
228
|
#: under no draws it only shapes how an existing gap closes.
|
|
233
229
|
TAU_GAP_PRIOR = "tau_gap_prior"
|
|
@@ -50,7 +50,12 @@ from zoneinfo import ZoneInfo
|
|
|
50
50
|
|
|
51
51
|
import numpy as np
|
|
52
52
|
import pandas as pd
|
|
53
|
-
from scipy.optimize import
|
|
53
|
+
from scipy.optimize import (
|
|
54
|
+
OptimizeResult,
|
|
55
|
+
least_squares,
|
|
56
|
+
minimize,
|
|
57
|
+
minimize_scalar,
|
|
58
|
+
)
|
|
54
59
|
|
|
55
60
|
from nwp500.sim.book import Booked, full_tank, house_gpm, is_full
|
|
56
61
|
from nwp500.sim.book import Step as BookStep
|
|
@@ -61,6 +66,7 @@ from nwp500.sim.heating_trajectory import path_cop
|
|
|
61
66
|
from nwp500.sim.inputs import ELECTRIC, ENERGY_SAVER, HIGH_DEMAND
|
|
62
67
|
from nwp500.sim.params import (
|
|
63
68
|
INSTALLATION_TERMS,
|
|
69
|
+
TAU_GAP_PRIOR_H,
|
|
64
70
|
UA_MIN_DELTA_T_F,
|
|
65
71
|
UA_REF_DELTA_T_F,
|
|
66
72
|
UNFITTED_TERMS,
|
|
@@ -104,6 +110,26 @@ _BAND_WIDTH_F = 20.0
|
|
|
104
110
|
MIN_GAP_FIT_START_F = 2.0
|
|
105
111
|
MIN_GAP_FIT_WINDOWS = 20
|
|
106
112
|
GAP_TAU_BOUNDS_HOURS = (1.0, 2000.0)
|
|
113
|
+
#: The loss law is fitted on windows opening with a gap of at most
|
|
114
|
+
#: this, °F: a wider one is a draw's front closing, the gap's constant's
|
|
115
|
+
#: regime, not the standby loss's.
|
|
116
|
+
MAX_LOSS_FIT_GAP_F = 10.0
|
|
117
|
+
#: A window opening with at least this gap, °F, shows the bottom loss
|
|
118
|
+
#: (``physics.tank``); the bottom loss is fitted only on this many.
|
|
119
|
+
MIN_BOTTOM_FIT_GAP_F = 1.0
|
|
120
|
+
MIN_BOTTOM_FIT_WINDOWS = 10
|
|
121
|
+
#: Bounds of the bottom loss, BTU/hr, and of its gate, °F.
|
|
122
|
+
BOTTOM_LOSS_BOUNDS = (0.0, 1000.0)
|
|
123
|
+
BOTTOM_GATE_BOUNDS = (0.05, 30.0)
|
|
124
|
+
#: The joint fit's soft_l1 scale, °F/h·√h: a window's end error
|
|
125
|
+
#: per hour, weighted by the root of its hours, past which a residual
|
|
126
|
+
#: counts linearly (``scipy.optimize.least_squares``' ``soft_l1``).
|
|
127
|
+
_LOSS_FIT_SCALE = 0.05
|
|
128
|
+
#: The gap's constant and the loss are fitted in turn
|
|
129
|
+
#: (``fit_standby``) until the constant moves less than this share, or
|
|
130
|
+
#: this many rounds.
|
|
131
|
+
_STANDBY_ROUNDS_RTOL = 0.005
|
|
132
|
+
_MAX_STANDBY_ROUNDS = 20
|
|
107
133
|
|
|
108
134
|
|
|
109
135
|
def _instants(frame: pd.DataFrame) -> list[datetime]:
|
|
@@ -191,6 +217,11 @@ def quiet_windows(
|
|
|
191
217
|
return out
|
|
192
218
|
|
|
193
219
|
|
|
220
|
+
def _no_series() -> np.ndarray:
|
|
221
|
+
"""An empty series: a window made without its probes."""
|
|
222
|
+
return np.zeros(0)
|
|
223
|
+
|
|
224
|
+
|
|
194
225
|
@dataclass(frozen=True)
|
|
195
226
|
class StandbyWindow:
|
|
196
227
|
"""One quiet window's fit: the UA that decays the tank's mean as
|
|
@@ -206,6 +237,12 @@ class StandbyWindow:
|
|
|
206
237
|
#: the stratification fit's input.
|
|
207
238
|
gap_hours: np.ndarray = field(repr=False)
|
|
208
239
|
gap_f: np.ndarray = field(repr=False)
|
|
240
|
+
#: The probes and the air on ``gap_hours``' grid, °F: the loss law's
|
|
241
|
+
#: input (``fit_loss``). Empty, a window made without them, is not
|
|
242
|
+
#: fitted on.
|
|
243
|
+
upper_f: np.ndarray = field(default_factory=_no_series, repr=False)
|
|
244
|
+
lower_f: np.ndarray = field(default_factory=_no_series, repr=False)
|
|
245
|
+
air_f: np.ndarray = field(default_factory=_no_series, repr=False)
|
|
209
246
|
|
|
210
247
|
|
|
211
248
|
def fit_window(
|
|
@@ -272,6 +309,9 @@ def fit_window(
|
|
|
272
309
|
duration_hours=(end - start).total_seconds() / 3600.0,
|
|
273
310
|
gap_hours=np.concatenate(([0.0], np.cumsum(dt_h))),
|
|
274
311
|
gap_f=(wide["upper_f"] - wide["lower_f"]).to_numpy(dtype=float),
|
|
312
|
+
upper_f=wide["upper_f"].to_numpy(dtype=float),
|
|
313
|
+
lower_f=wide["lower_f"].to_numpy(dtype=float),
|
|
314
|
+
air_f=air,
|
|
275
315
|
)
|
|
276
316
|
|
|
277
317
|
|
|
@@ -313,16 +353,55 @@ def fit_ua_law(
|
|
|
313
353
|
return float(np.exp(intercept)), exponent
|
|
314
354
|
|
|
315
355
|
|
|
356
|
+
def gap_paths(
|
|
357
|
+
windows: Sequence[tuple[np.ndarray, np.ndarray]],
|
|
358
|
+
tau_gap_hours: float,
|
|
359
|
+
loss: LossParams | None = None,
|
|
360
|
+
tank: TankParams = TankParams(),
|
|
361
|
+
) -> list[np.ndarray]:
|
|
362
|
+
"""Each window's gap, ``(hours, gap)``, as the standby walk takes it
|
|
363
|
+
from its first value: relaxing on ``tau_gap_hours``, and opened by
|
|
364
|
+
``loss``' bottom loss (``physics.tank``) where it has one. Without
|
|
365
|
+
one, the closed form ``gap_0 exp(-t / tau)``."""
|
|
366
|
+
if loss is None or loss.bottom_loss_btu_hr <= 0.0:
|
|
367
|
+
return [g[0] * np.exp(-t / tau_gap_hours) for t, g in windows]
|
|
368
|
+
q, gate = loss.bottom_loss_btu_hr, loss.bottom_gate_f
|
|
369
|
+
lower_mass = tank.lower_zone_thermal_mass_btu_f
|
|
370
|
+
n = len(windows)
|
|
371
|
+
if n == 0:
|
|
372
|
+
return []
|
|
373
|
+
steps = max(len(g) for _, g in windows) - 1
|
|
374
|
+
dt = np.zeros((n, max(steps, 0)))
|
|
375
|
+
for i, (t, _) in enumerate(windows):
|
|
376
|
+
dt[i, : len(t) - 1] = np.diff(np.asarray(t, dtype=float))
|
|
377
|
+
gap = np.maximum(np.array([g[0] for _, g in windows], dtype=float), 0.0)
|
|
378
|
+
path = np.zeros((n, steps + 1))
|
|
379
|
+
path[:, 0] = gap
|
|
380
|
+
for j in range(steps):
|
|
381
|
+
step = dt[:, j]
|
|
382
|
+
if gate > 0.0:
|
|
383
|
+
# The bottom loss at the step's start, as the walk charges it.
|
|
384
|
+
gap = gap + q * (1.0 - np.exp(-gap / gate)) * step / lower_mass
|
|
385
|
+
gap = gap * np.exp(-step / tau_gap_hours)
|
|
386
|
+
path[:, j + 1] = gap
|
|
387
|
+
return [path[i, : len(g)] for i, (_, g) in enumerate(windows)]
|
|
388
|
+
|
|
389
|
+
|
|
316
390
|
def fit_gap_tau(
|
|
317
391
|
windows: Sequence[tuple[np.ndarray, np.ndarray]],
|
|
318
392
|
min_start_gap_f: float = MIN_GAP_FIT_START_F,
|
|
319
393
|
min_windows: int = MIN_GAP_FIT_WINDOWS,
|
|
394
|
+
*,
|
|
395
|
+
loss: LossParams | None = None,
|
|
396
|
+
tank: TankParams = TankParams(),
|
|
320
397
|
) -> tuple[float | None, int]:
|
|
321
398
|
"""The time constant the stratification gap relaxes on, hours: one
|
|
322
|
-
constant minimising the squared gap error of ``
|
|
323
|
-
pooled over every sample of
|
|
324
|
-
|
|
325
|
-
``
|
|
399
|
+
constant minimising the squared gap error of ``gap_paths`` (with no
|
|
400
|
+
bottom loss, ``gap_0 exp(-t/tau)``) pooled over every sample of
|
|
401
|
+
every window starting with a gap of at least ``min_start_gap_f``.
|
|
402
|
+
``loss``' bottom loss opens the gaps it relaxes, as the walk does,
|
|
403
|
+
so a gap it widens is not read as one relaxing slowly. ``(None, n)``
|
|
404
|
+
with fewer than ``min_windows``."""
|
|
326
405
|
used = [
|
|
327
406
|
(t, g)
|
|
328
407
|
for t, g in windows
|
|
@@ -332,9 +411,10 @@ def fit_gap_tau(
|
|
|
332
411
|
return None, len(used)
|
|
333
412
|
|
|
334
413
|
def sse(log_tau: float) -> float:
|
|
335
|
-
|
|
414
|
+
paths = gap_paths(used, float(np.exp(log_tau)), loss, tank)
|
|
336
415
|
return sum(
|
|
337
|
-
float(np.sum((g -
|
|
416
|
+
float(np.sum((g - path) ** 2))
|
|
417
|
+
for (_, g), path in zip(used, paths, strict=True)
|
|
338
418
|
)
|
|
339
419
|
|
|
340
420
|
low, high = GAP_TAU_BOUNDS_HOURS
|
|
@@ -347,6 +427,225 @@ def fit_gap_tau(
|
|
|
347
427
|
return float(np.exp(found.x)), len(used)
|
|
348
428
|
|
|
349
429
|
|
|
430
|
+
def gap_error(
|
|
431
|
+
windows: Sequence[StandbyWindow],
|
|
432
|
+
tau_gap_hours: float | None,
|
|
433
|
+
loss: LossParams | None = None,
|
|
434
|
+
tank: TankParams = TankParams(),
|
|
435
|
+
) -> float | None:
|
|
436
|
+
"""The gap's mean squared error on ``windows``, °F², as
|
|
437
|
+
``gap_paths`` takes it. None with no window or no constant."""
|
|
438
|
+
if not windows or tau_gap_hours is None or not tau_gap_hours > 0.0:
|
|
439
|
+
return None
|
|
440
|
+
pairs = [(w.gap_hours, w.gap_f) for w in windows]
|
|
441
|
+
paths = gap_paths(pairs, tau_gap_hours, loss, tank)
|
|
442
|
+
squared = [
|
|
443
|
+
(g - path) ** 2 for (_, g), path in zip(pairs, paths, strict=True)
|
|
444
|
+
]
|
|
445
|
+
return float(np.mean(np.concatenate(squared)))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def walk_tau(tank: TankParams, tau: float | None = None) -> float:
|
|
449
|
+
"""The gap's constant the standby loss is walked on, hours: ``tau``,
|
|
450
|
+
else ``tank``'s, else ``TAU_GAP_PRIOR_H``, as the simulation serves
|
|
451
|
+
an unfitted one."""
|
|
452
|
+
if tau is not None:
|
|
453
|
+
return tau
|
|
454
|
+
if tank.stratification_gap_tau_h is not None:
|
|
455
|
+
return tank.stratification_gap_tau_h
|
|
456
|
+
return TAU_GAP_PRIOR_H
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def loss_windows(
|
|
460
|
+
windows: Sequence[StandbyWindow],
|
|
461
|
+
) -> list[StandbyWindow]:
|
|
462
|
+
"""The windows ``fit_loss`` fits on: with their probes and air,
|
|
463
|
+
opening with a gap of at most ``MAX_LOSS_FIT_GAP_F``, and a UA, a
|
|
464
|
+
ΔT and hours above zero (``fit_ua_law``'s own test)."""
|
|
465
|
+
return [
|
|
466
|
+
w
|
|
467
|
+
for w in windows
|
|
468
|
+
if len(w.upper_f) >= 2
|
|
469
|
+
and len(w.upper_f) == len(w.lower_f) == len(w.air_f)
|
|
470
|
+
and len(w.gap_hours) == len(w.upper_f)
|
|
471
|
+
and bool(np.isfinite(w.upper_f).all())
|
|
472
|
+
and bool(np.isfinite(w.lower_f).all())
|
|
473
|
+
and bool(np.isfinite(w.air_f).all())
|
|
474
|
+
and w.upper_f[0] - w.lower_f[0] <= MAX_LOSS_FIT_GAP_F
|
|
475
|
+
and w.ua_btu_hr_f > 0.0
|
|
476
|
+
and w.delta_t_mean_f > 0.0
|
|
477
|
+
and w.duration_hours > 0.0
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def standby_ends(
|
|
482
|
+
windows: Sequence[StandbyWindow],
|
|
483
|
+
loss: LossParams,
|
|
484
|
+
tau_gap_hours: float,
|
|
485
|
+
tank: TankParams = TankParams(),
|
|
486
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
487
|
+
"""Each window's upper and lower probe at its end, °F, as the
|
|
488
|
+
standby walk (``decay.project_decay``) takes them there from its
|
|
489
|
+
first reading on ``loss``: the UA law on the bulk, the bottom loss
|
|
490
|
+
on the lower zone, the gap relaxing on ``tau_gap_hours``. Windows
|
|
491
|
+
from ``loss_windows``; a UA of None is ``ua_baseline_btu_hr_f``."""
|
|
492
|
+
n = len(windows)
|
|
493
|
+
if n == 0:
|
|
494
|
+
return np.zeros(0), np.zeros(0)
|
|
495
|
+
steps = max(len(w.upper_f) for w in windows) - 1
|
|
496
|
+
dt = np.zeros((n, steps))
|
|
497
|
+
air = np.zeros((n, steps))
|
|
498
|
+
for i, w in enumerate(windows):
|
|
499
|
+
k = len(w.upper_f) - 1
|
|
500
|
+
dt[i, :k] = np.diff(np.asarray(w.gap_hours, dtype=float))
|
|
501
|
+
air[i, :k] = w.air_f[:k]
|
|
502
|
+
ua_ref = (
|
|
503
|
+
loss.ua_baseline_btu_hr_f
|
|
504
|
+
if loss.ua_btu_hr_f is None
|
|
505
|
+
else loss.ua_btu_hr_f
|
|
506
|
+
)
|
|
507
|
+
mass = tank.thermal_mass_btu_f
|
|
508
|
+
lower_mass = tank.lower_zone_thermal_mass_btu_f
|
|
509
|
+
upper0 = np.array([w.upper_f[0] for w in windows], dtype=float)
|
|
510
|
+
lower0 = np.array([w.lower_f[0] for w in windows], dtype=float)
|
|
511
|
+
bulk = (upper0 + lower0) / 2.0
|
|
512
|
+
gap = np.maximum(upper0 - lower0, 0.0)
|
|
513
|
+
q, gate = loss.bottom_loss_btu_hr, loss.bottom_gate_f
|
|
514
|
+
for j in range(steps):
|
|
515
|
+
step = dt[:, j]
|
|
516
|
+
if q > 0.0 and gate > 0.0:
|
|
517
|
+
# At the step's start, as the walk charges it.
|
|
518
|
+
drop = q * (1.0 - np.exp(-gap / gate)) * step / lower_mass
|
|
519
|
+
bulk = bulk - drop / 2.0
|
|
520
|
+
gap = gap + drop
|
|
521
|
+
delta = bulk - air[:, j]
|
|
522
|
+
if loss.ua_exponent == 0.0:
|
|
523
|
+
ua = np.full(n, ua_ref)
|
|
524
|
+
else:
|
|
525
|
+
scaled = (
|
|
526
|
+
np.maximum(np.abs(delta), UA_MIN_DELTA_T_F)
|
|
527
|
+
/ loss.ua_ref_delta_t_f
|
|
528
|
+
)
|
|
529
|
+
ua = ua_ref * scaled**loss.ua_exponent
|
|
530
|
+
bulk = bulk - ua / mass * delta * step
|
|
531
|
+
gap = gap * np.exp(-step / tau_gap_hours)
|
|
532
|
+
return bulk + gap / 2.0, bulk - gap / 2.0
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def standby_error(
|
|
536
|
+
windows: Sequence[StandbyWindow],
|
|
537
|
+
loss: LossParams,
|
|
538
|
+
tau_gap_hours: float,
|
|
539
|
+
tank: TankParams = TankParams(),
|
|
540
|
+
) -> float | None:
|
|
541
|
+
"""The probes' mean absolute error in each window's fall, °F/h:
|
|
542
|
+
``standby_ends`` against each window's last readings, both probes
|
|
543
|
+
alike. None with no window."""
|
|
544
|
+
if not windows:
|
|
545
|
+
return None
|
|
546
|
+
upper, lower = standby_ends(windows, loss, tau_gap_hours, tank)
|
|
547
|
+
hours = np.array([w.duration_hours for w in windows], dtype=float)
|
|
548
|
+
seen_u = np.array([w.upper_f[-1] for w in windows], dtype=float)
|
|
549
|
+
seen_l = np.array([w.lower_f[-1] for w in windows], dtype=float)
|
|
550
|
+
errors = np.concatenate(
|
|
551
|
+
[np.abs(upper - seen_u) / hours, np.abs(lower - seen_l) / hours]
|
|
552
|
+
)
|
|
553
|
+
return float(np.mean(errors))
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@dataclass(frozen=True)
|
|
557
|
+
class LossFit:
|
|
558
|
+
"""The loss law ``fit_loss`` found: the UA law's pair, the bottom
|
|
559
|
+
loss and its gate (0.0 and None without the evidence for them), and
|
|
560
|
+
the windows it stood on, and of those, the stratified ones."""
|
|
561
|
+
|
|
562
|
+
ua_ref: float
|
|
563
|
+
ua_exponent: float
|
|
564
|
+
bottom_loss_btu_hr: float
|
|
565
|
+
bottom_gate_f: float | None
|
|
566
|
+
n_windows: int
|
|
567
|
+
n_stratified: int
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def fit_loss(
|
|
571
|
+
windows: Sequence[StandbyWindow],
|
|
572
|
+
tau_gap_hours: float,
|
|
573
|
+
tank: TankParams = TankParams(),
|
|
574
|
+
) -> LossFit | None:
|
|
575
|
+
"""The standby loss on ``windows`` (``loss_windows`` of them).
|
|
576
|
+
|
|
577
|
+
With ``MIN_BOTTOM_FIT_WINDOWS`` opening with a gap of at least
|
|
578
|
+
``MIN_BOTTOM_FIT_GAP_F``, the UA law and the bottom loss are fitted
|
|
579
|
+
together through the standby walk (``standby_ends``): each window's
|
|
580
|
+
upper and lower probe at its end against what it read, per hour,
|
|
581
|
+
weighted by the root of its hours, ``soft_l1``. A mixed window
|
|
582
|
+
holds the UA law; a stratified one, the bottom loss and its gate
|
|
583
|
+
(``physics.tank``). The exponent is fitted only where ``fit_ua_law``
|
|
584
|
+
would fit one. Without that evidence, ``fit_ua_law``'s law and no
|
|
585
|
+
bottom loss. None with no window."""
|
|
586
|
+
used = loss_windows(windows)
|
|
587
|
+
if not used:
|
|
588
|
+
return None
|
|
589
|
+
ua_ref, exponent = fit_ua_law(
|
|
590
|
+
[w.delta_t_mean_f for w in used],
|
|
591
|
+
[w.ua_btu_hr_f for w in used],
|
|
592
|
+
[w.duration_hours for w in used],
|
|
593
|
+
)
|
|
594
|
+
stratified = [
|
|
595
|
+
w for w in used if w.upper_f[0] - w.lower_f[0] >= MIN_BOTTOM_FIT_GAP_F
|
|
596
|
+
]
|
|
597
|
+
if len(stratified) < MIN_BOTTOM_FIT_WINDOWS:
|
|
598
|
+
return LossFit(ua_ref, exponent, 0.0, None, len(used), len(stratified))
|
|
599
|
+
bands = np.floor(np.array([w.delta_t_mean_f for w in used]) / _BAND_WIDTH_F)
|
|
600
|
+
with_exponent = (
|
|
601
|
+
len(used) >= _MIN_WINDOWS_FOR_LAW
|
|
602
|
+
and len(np.unique(bands)) >= _MIN_BANDS_FOR_LAW
|
|
603
|
+
)
|
|
604
|
+
hours = np.array([w.duration_hours for w in used], dtype=float)
|
|
605
|
+
seen_u = np.array([w.upper_f[-1] for w in used], dtype=float)
|
|
606
|
+
seen_l = np.array([w.lower_f[-1] for w in used], dtype=float)
|
|
607
|
+
weight = np.sqrt(hours) / hours
|
|
608
|
+
|
|
609
|
+
def law(x: np.ndarray) -> LossParams:
|
|
610
|
+
return LossParams(
|
|
611
|
+
ua_btu_hr_f=float(x[0]),
|
|
612
|
+
ua_exponent=float(x[3]) if with_exponent else 0.0,
|
|
613
|
+
bottom_loss_btu_hr=float(x[1]),
|
|
614
|
+
bottom_gate_f=float(x[2]),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
def residuals(x: np.ndarray) -> np.ndarray:
|
|
618
|
+
upper, lower = standby_ends(used, law(x), tau_gap_hours, tank)
|
|
619
|
+
return np.concatenate(
|
|
620
|
+
[(upper - seen_u) * weight, (lower - seen_l) * weight]
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
lo = [UA_SEARCH_BOUNDS[0], BOTTOM_LOSS_BOUNDS[0], BOTTOM_GATE_BOUNDS[0]]
|
|
624
|
+
hi = [UA_SEARCH_BOUNDS[1], BOTTOM_LOSS_BOUNDS[1], BOTTOM_GATE_BOUNDS[1]]
|
|
625
|
+
x0 = [ua_ref, 50.0, 1.0]
|
|
626
|
+
if with_exponent:
|
|
627
|
+
lo.append(UA_EXPONENT_BOUNDS[0])
|
|
628
|
+
hi.append(UA_EXPONENT_BOUNDS[1])
|
|
629
|
+
x0.append(min(max(exponent, 0.05), UA_EXPONENT_BOUNDS[1] - 0.05))
|
|
630
|
+
x0[0] = min(max(x0[0], lo[0] + 1e-6), hi[0] - 1e-6)
|
|
631
|
+
found = least_squares(
|
|
632
|
+
residuals,
|
|
633
|
+
np.array(x0, dtype=float),
|
|
634
|
+
bounds=(np.array(lo), np.array(hi)),
|
|
635
|
+
loss="soft_l1",
|
|
636
|
+
f_scale=_LOSS_FIT_SCALE,
|
|
637
|
+
)
|
|
638
|
+
fitted = law(found.x)
|
|
639
|
+
return LossFit(
|
|
640
|
+
cast(float, fitted.ua_btu_hr_f),
|
|
641
|
+
fitted.ua_exponent,
|
|
642
|
+
fitted.bottom_loss_btu_hr,
|
|
643
|
+
fitted.bottom_gate_f,
|
|
644
|
+
len(used),
|
|
645
|
+
len(stratified),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
350
649
|
@dataclass(frozen=True)
|
|
351
650
|
class StandbyFit:
|
|
352
651
|
"""The standby fits: the windows kept, the law, the gap's constant,
|
|
@@ -364,6 +663,12 @@ class StandbyFit:
|
|
|
364
663
|
#: The quiet test: ``meter`` with draws and circulation recorded,
|
|
365
664
|
#: ``dhw_in_use`` without them, ``meter+dhw_in_use`` with one.
|
|
366
665
|
quiet_by: str
|
|
666
|
+
#: The bottom loss and its gate (``fit_loss``): 0.0 and None
|
|
667
|
+
#: without the stratified windows to fit them on, and how many
|
|
668
|
+
#: there were.
|
|
669
|
+
bottom_loss_btu_hr: float = 0.0
|
|
670
|
+
bottom_gate_f: float | None = None
|
|
671
|
+
n_stratified: int = 0
|
|
367
672
|
|
|
368
673
|
|
|
369
674
|
def fit_standby(
|
|
@@ -372,34 +677,63 @@ def fit_standby(
|
|
|
372
677
|
*,
|
|
373
678
|
min_quiet_hours: float = MIN_QUIET_HOURS,
|
|
374
679
|
settle_minutes: float = SETTLE_MINUTES,
|
|
680
|
+
tank: TankParams | None = None,
|
|
681
|
+
loss: LossParams | None = None,
|
|
375
682
|
) -> StandbyFit:
|
|
376
|
-
"""The standby loss
|
|
377
|
-
|
|
683
|
+
"""The standby loss and the gap's time constant from the history's
|
|
684
|
+
quiet windows: the gap's constant first, then the loss
|
|
685
|
+
(``fit_loss``) walked on it, or on ``tank``'s where too few windows
|
|
686
|
+
open with a gap to fit one. ``tank`` (default the default tank of
|
|
687
|
+
``thermal_mass_btu_f``'s mass) is the walk's lower zone. ``loss``
|
|
688
|
+
is the loss the parameters hold: where no loss is fitted (no window
|
|
689
|
+
the loss can use), its bottom loss opens the gaps the constant is
|
|
690
|
+
fitted on, as the simulation will run them."""
|
|
691
|
+
if tank is None:
|
|
692
|
+
tank = replace(
|
|
693
|
+
TankParams(),
|
|
694
|
+
volume_gal=thermal_mass_btu_f / WATER_BTU_PER_GAL_F,
|
|
695
|
+
)
|
|
378
696
|
quiet = quiet_windows(history, min_quiet_hours, settle_minutes)
|
|
379
697
|
kept: list[StandbyWindow] = []
|
|
380
698
|
for window in quiet:
|
|
381
699
|
fit = fit_window(history, window, thermal_mass_btu_f)
|
|
382
700
|
if fit is not None and fit.rmse_f < MAX_WINDOW_RMSE_F:
|
|
383
701
|
kept.append(fit)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
702
|
+
# The gap's constant and the bottom loss each shape the other's
|
|
703
|
+
# evidence: the loss is walked on the constant, and the bottom loss
|
|
704
|
+
# opens the gaps the constant relaxes. Fitted in turn, the constant
|
|
705
|
+
# first on the gaps alone, until the constant settles.
|
|
706
|
+
gaps = [(w.gap_hours, w.gap_f) for w in kept]
|
|
707
|
+
tau, n_gap = fit_gap_tau(gaps)
|
|
708
|
+
found = fit_loss(kept, walk_tau(tank, tau), tank)
|
|
709
|
+
if found is None and loss is not None and loss.bottom_loss_btu_hr > 0.0:
|
|
710
|
+
# No loss to fit: the parameters keep theirs, so the constant is
|
|
711
|
+
# fitted on the gaps their bottom loss opens.
|
|
712
|
+
tau, n_gap = fit_gap_tau(gaps, loss=loss, tank=tank)
|
|
713
|
+
for _ in range(_MAX_STANDBY_ROUNDS):
|
|
714
|
+
if found is None or found.bottom_gate_f is None or tau is None:
|
|
715
|
+
break
|
|
716
|
+
opened = LossParams(
|
|
717
|
+
bottom_loss_btu_hr=found.bottom_loss_btu_hr,
|
|
718
|
+
bottom_gate_f=found.bottom_gate_f,
|
|
392
719
|
)
|
|
393
|
-
|
|
720
|
+
before = tau
|
|
721
|
+
tau, n_gap = fit_gap_tau(gaps, loss=opened, tank=tank)
|
|
722
|
+
found = fit_loss(kept, walk_tau(tank, tau), tank)
|
|
723
|
+
if tau is None or abs(tau - before) <= _STANDBY_ROUNDS_RTOL * before:
|
|
724
|
+
break
|
|
394
725
|
tier1 = [c for c in ("drawing", "circulating") if c in history.columns]
|
|
395
726
|
return StandbyFit(
|
|
396
727
|
windows=tuple(kept),
|
|
397
728
|
n_quiet=len(quiet),
|
|
398
729
|
n_dropped=len(quiet) - len(kept),
|
|
399
|
-
ua_ref=ua_ref,
|
|
400
|
-
ua_exponent=
|
|
730
|
+
ua_ref=None if found is None else found.ua_ref,
|
|
731
|
+
ua_exponent=None if found is None else found.ua_exponent,
|
|
401
732
|
tau_gap_hours=tau,
|
|
402
733
|
n_gap_windows=n_gap,
|
|
734
|
+
bottom_loss_btu_hr=0.0 if found is None else found.bottom_loss_btu_hr,
|
|
735
|
+
bottom_gate_f=None if found is None else found.bottom_gate_f,
|
|
736
|
+
n_stratified=0 if found is None else found.n_stratified,
|
|
403
737
|
quiet_by=(
|
|
404
738
|
"meter"
|
|
405
739
|
if len(tier1) == 2
|
|
@@ -997,12 +1331,20 @@ def with_standby(params: HeaterParams, fit: StandbyFit) -> HeaterParams:
|
|
|
997
1331
|
loss, tank = params.loss, params.tank
|
|
998
1332
|
if fit.ua_ref is not None and fit.ua_exponent is not None:
|
|
999
1333
|
# Quoted at the ΔT ``fit_standby``'s law was fitted at, not
|
|
1000
|
-
# whatever ``params`` held.
|
|
1334
|
+
# whatever ``params`` held. The bottom loss goes with it: a law
|
|
1335
|
+
# fitted without one has the bottom's heat in its UA, so the
|
|
1336
|
+
# parameters' own would count it twice.
|
|
1001
1337
|
loss = replace(
|
|
1002
1338
|
loss,
|
|
1003
1339
|
ua_btu_hr_f=fit.ua_ref,
|
|
1004
1340
|
ua_exponent=fit.ua_exponent,
|
|
1005
1341
|
ua_ref_delta_t_f=UA_REF_DELTA_T_F,
|
|
1342
|
+
bottom_loss_btu_hr=fit.bottom_loss_btu_hr,
|
|
1343
|
+
bottom_gate_f=(
|
|
1344
|
+
loss.bottom_gate_f
|
|
1345
|
+
if fit.bottom_gate_f is None
|
|
1346
|
+
else fit.bottom_gate_f
|
|
1347
|
+
),
|
|
1006
1348
|
)
|
|
1007
1349
|
if fit.tau_gap_hours is not None:
|
|
1008
1350
|
tank = replace(tank, stratification_gap_tau_h=fit.tau_gap_hours)
|
|
@@ -1544,7 +1886,9 @@ def calibrate(
|
|
|
1544
1886
|
``gap_tau``. ``base``'s record of the installation's terms
|
|
1545
1887
|
(``INSTALLATION_TERMS``) is kept: they are fitted apart, and a
|
|
1546
1888
|
caller applying its own fits drops each from ``unfitted``."""
|
|
1547
|
-
standby = fit_standby(
|
|
1889
|
+
standby = fit_standby(
|
|
1890
|
+
history, base.tank.thermal_mass_btu_f, tank=base.tank, loss=base.loss
|
|
1891
|
+
)
|
|
1548
1892
|
params = with_standby(base, standby)
|
|
1549
1893
|
rows = snapshots(
|
|
1550
1894
|
history, air=recovery_air, draws=draws, circulation=circulation
|