aark 0.1.0__py3-none-any.whl

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.
aark/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """airallergy's research kit."""
2
+
3
+ __version__ = "0.1.0"
aark/ncm/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """National Calculation Methodology (NCM): https://www.uk-ncm.org.uk/."""
aark/ncm/sched.py ADDED
@@ -0,0 +1,511 @@
1
+ """Convert NCM schedules into epJSON objects.
2
+
3
+ Caveats:
4
+ - a non-leap year is assumed
5
+ - other gains schedules are not converted
6
+ """
7
+
8
+ import datetime
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterable, Sequence
13
+
14
+ import pyodbc
15
+
16
+ type PyODBCRows = list[pyodbc.Row]
17
+ type PyODBCCursor = pyodbc.Cursor
18
+ type SchedMap = dict[int, dict[str, set[str]]]
19
+ type epJSONObjBody = dict[str, object] # noqa: N816, PYI042
20
+ type epJSONObjs = dict[str, epJSONObjBody] # noqa: N816, PYI042
21
+
22
+
23
+ ACTIVITY_SCHED_COLUMN_NAMES = (
24
+ "OCCUPANCY_SCH",
25
+ "LIGHTING_SCH",
26
+ "EQUIPMENT_SCH",
27
+ "COOL_SET_SCH",
28
+ "HEAT_SET_SCH",
29
+ # "OTHER_GAINS_SCH", # secondary schedule id to look up in [activity_other_gains]
30
+ )
31
+
32
+ WEEKLY_SCHED_DAY_TYPES = (
33
+ "MONDAY",
34
+ "TUESDAY",
35
+ "WEDNESDAY",
36
+ "THURSDAY",
37
+ "FRIDAY",
38
+ "SATURDAY",
39
+ "SUNDAY",
40
+ "HOLIDAY",
41
+ )
42
+
43
+ MONTH_ABBR2NUM = {
44
+ "Jan": 1,
45
+ "Feb": 2,
46
+ "Mar": 3,
47
+ "Apr": 4,
48
+ "May": 5,
49
+ "Jun": 6,
50
+ "Jul": 7,
51
+ "Aug": 8,
52
+ "Sep": 9,
53
+ "Oct": 10,
54
+ "Nov": 11,
55
+ "Dec": 12,
56
+ }
57
+
58
+ SCHED_TYPE_LIMITS_OBJS = {
59
+ "FRACTION": {
60
+ "lower_limit_value": 0,
61
+ "upper_limit_value": 1,
62
+ "numeric_type": "Continuous",
63
+ "unit_type": "Dimensionless",
64
+ },
65
+ "ON/OFF": {
66
+ "lower_limit_value": 0,
67
+ "upper_limit_value": 1,
68
+ "numeric_type": "Discrete",
69
+ "unit_type": "Availability",
70
+ },
71
+ "TEMPERATURE": {
72
+ "lower_limit_value": -100,
73
+ "upper_limit_value": 100,
74
+ "numeric_type": "Continuous",
75
+ "unit_type": "Temperature",
76
+ },
77
+ }
78
+
79
+
80
+ def _fetch_filter(
81
+ table_name: str,
82
+ column_name: str,
83
+ column_values: Iterable[object],
84
+ cursor: PyODBCCursor,
85
+ ) -> PyODBCRows:
86
+ """Fetch rows of a table filtered by values in a given column.
87
+
88
+ NOTE: this fetch and filter approach is used instead of a SQL query with a WHERE
89
+ clause, because some ODBC driver apears to have a limit on the number of column
90
+ values to be filtered via WHERE.
91
+ """
92
+ column_values = set(column_values)
93
+
94
+ cursor.execute(f"SELECT * FROM [{table_name}]")
95
+ rows = cursor.fetchall()
96
+ return [row for row in rows if getattr(row, column_name) in column_values]
97
+
98
+
99
+ def _next_month_day(month: int, day: int) -> tuple[int, int]:
100
+ """Return the next month and day given a month and day.
101
+
102
+ NOTE: this function hardcodes a non-leap year.
103
+ """
104
+ date = datetime.date(2026, month, day) + datetime.timedelta(days=1)
105
+ return date.month, date.day
106
+
107
+
108
+ def _add_sched_map(
109
+ sched_map: SchedMap, annual_sched_id: int, ep_obj_type: str, ep_obj_name: str
110
+ ) -> None:
111
+ """Add mapping to an epJSON object name by annual schedule id and EP object type."""
112
+ sched_map.setdefault(annual_sched_id, {})
113
+ sched_map[annual_sched_id].setdefault(ep_obj_type, set())
114
+ sched_map[annual_sched_id][ep_obj_type].add(ep_obj_name)
115
+
116
+
117
+ def _add_epjson_obj(
118
+ epjson_objs: epJSONObjs, ep_obj_name: str, epjson_obj_body: epJSONObjBody
119
+ ) -> None:
120
+ """Add an epJSON object."""
121
+ if (ep_obj_name in epjson_objs) and (epjson_objs[ep_obj_name] != epjson_obj_body):
122
+ raise ValueError(f"object key collision with different content: {ep_obj_name}")
123
+
124
+ epjson_objs.update({ep_obj_name: epjson_obj_body})
125
+
126
+
127
+ def read_scheds(
128
+ activity_rows: PyODBCRows, cursor: PyODBCCursor
129
+ ) -> tuple[PyODBCRows, PyODBCRows, PyODBCRows, PyODBCRows, PyODBCRows]:
130
+ """Read NCM activity schedule data.
131
+
132
+ Parameters
133
+ ----------
134
+ activity_rows : PyODBCRows
135
+ Rows of the `[activity]` table.
136
+ cursor : PyODBCCursor
137
+ Open cursor of the NCM activity database.
138
+
139
+ Returns
140
+ -------
141
+ tuple[PyODBCRows, PyODBCRows, PyODBCRows, PyODBCRows, PyODBCRows]
142
+ A tuple containing:
143
+ - rows of the `[annual_schedules]` table.
144
+ - rows of the `[annual_weekly_schedules]` table.
145
+ - rows of the `[weekly_schedules]` table.
146
+ - rows of the `[daily_schedules]` table.
147
+ - rows of the `[schedules_type]` table.
148
+ """
149
+ # get schedule type rows
150
+ cursor.execute("SELECT * FROM [schedules_type]")
151
+ sched_type_rows = cursor.fetchall()
152
+
153
+ # get annual schedule ids used in the [activity] table
154
+ annual_sched_ids = {
155
+ getattr(row, column_name)
156
+ for row in activity_rows
157
+ for column_name in ACTIVITY_SCHED_COLUMN_NAMES
158
+ }
159
+
160
+ # get annual schedule rows
161
+ annual_sched_rows = _fetch_filter(
162
+ "annual_schedules", "ID", annual_sched_ids, cursor
163
+ )
164
+
165
+ # get annual weekly schedule rows
166
+ # note that annual schedules work differently from weekly and daily schedules
167
+ # as they have an indefinite number of segments
168
+ annual_weekly_sched_rows = _fetch_filter(
169
+ "annual_weekly_schedules", "ANNUAL_SCHEDULE", annual_sched_ids, cursor
170
+ )
171
+
172
+ # get weekly schedule ids used in the [annual_weekly_schedules] table
173
+ weekly_sched_ids = {row.WEEKLY_SCHEDULE for row in annual_weekly_sched_rows}
174
+
175
+ # get weekly schedule rows
176
+ weekly_sched_rows = _fetch_filter(
177
+ "weekly_schedules", "ID", weekly_sched_ids, cursor
178
+ )
179
+
180
+ # get daily schedule ids used in the [weekly_schedules] table
181
+ daily_sched_ids = {
182
+ getattr(row, day_type)
183
+ for row in weekly_sched_rows
184
+ for day_type in WEEKLY_SCHED_DAY_TYPES
185
+ }
186
+
187
+ # get daily schedule rows
188
+ daily_sched_rows = _fetch_filter("daily_schedules", "ID", daily_sched_ids, cursor)
189
+
190
+ return (
191
+ annual_sched_rows,
192
+ annual_weekly_sched_rows,
193
+ weekly_sched_rows,
194
+ daily_sched_rows,
195
+ sched_type_rows,
196
+ )
197
+
198
+
199
+ def convert_scheds( # noqa: PLR0915
200
+ annual_sched_rows: PyODBCRows,
201
+ annual_weekly_sched_rows: PyODBCRows,
202
+ weekly_sched_rows: PyODBCRows,
203
+ daily_sched_rows: PyODBCRows,
204
+ sched_type_rows: PyODBCRows,
205
+ ) -> tuple[SchedMap, epJSONObjs]:
206
+ """Convert NCM schedule data into an epJSON schedule library.
207
+
208
+ Parameters
209
+ ----------
210
+ annual_sched_rows : PyODBCRows
211
+ Rows of the `[annual_schedules]` table.
212
+ annual_weekly_sched_rows : PyODBCRows
213
+ Rows of the `[annual_weekly_schedules]` table.
214
+ weekly_sched_rows : PyODBCRows
215
+ Rows of the `[weekly_schedules]` table.
216
+ daily_sched_rows : PyODBCRows
217
+ Rows of the `[daily_schedules]` table.
218
+ sched_type_rows : PyODBCRows
219
+ Rows of the `[schedules_type]` table.
220
+
221
+ Returns
222
+ -------
223
+ tuple[SchedMap, epJSONObjs]
224
+ An epJSON schedule library containing:
225
+ - a map of annual schedule ids to epJSON object names grouped by EP object type.
226
+ - a map of epJSON object names to epJSON object bodies.
227
+ """
228
+ # -----------------------------------------------------------------------------
229
+ # 1) create maps of ids to names
230
+ # -----------------------------------------------------------------------------
231
+
232
+ # create a map of schedule type ids to names
233
+ sched_type_id2name = {row.ID: row.COD for row in sched_type_rows}
234
+
235
+ # create a map of ncm ids to ep object names for annual schedules
236
+ annual_sched_id2name = {
237
+ row.ID: f"ncm-annual-{row.ID}-{row.NAME}" for row in annual_sched_rows
238
+ }
239
+
240
+ # create a map of ncm ids to ep object names for weekly schedules
241
+ weekly_sched_id2name = {
242
+ row.ID: f"ncm-weekly-{row.ID}-{row.NAME}" for row in weekly_sched_rows
243
+ }
244
+
245
+ # create a map of ncm ids to ep object names for daily schedules
246
+ daily_sched_id2name = {
247
+ row.ID: f"ncm-daily-{row.ID}-{row.NAME}" for row in daily_sched_rows
248
+ }
249
+
250
+ # -----------------------------------------------------------------------------
251
+ # 2) create a map of annual schedule ids to epJSON object names by object type
252
+ # -----------------------------------------------------------------------------
253
+
254
+ sched_map: SchedMap = {}
255
+
256
+ for annual_sched_row in annual_sched_rows:
257
+ # get and add the annual schedule's ep object name
258
+ # to the `Schedule:Year` set
259
+ annual_sched_id = annual_sched_row.ID
260
+ annual_sched_name = annual_sched_id2name[annual_sched_id]
261
+
262
+ _add_sched_map(sched_map, annual_sched_id, "Schedule:Year", annual_sched_name)
263
+
264
+ # get and add the annual schedule type's ep object name
265
+ annual_sched_type_name = sched_type_id2name[annual_sched_row.TYPE]
266
+ _add_sched_map(
267
+ sched_map, annual_sched_id, "ScheduleTypeLimits", annual_sched_type_name
268
+ )
269
+
270
+ for annual_weekly_sched_row in annual_weekly_sched_rows:
271
+ if annual_weekly_sched_row.ANNUAL_SCHEDULE != annual_sched_id:
272
+ continue
273
+
274
+ # get and add the weekly schedule's ep object name
275
+ # to the `Schedule:Week:Daily` set
276
+ weekly_sched_id = annual_weekly_sched_row.WEEKLY_SCHEDULE
277
+ weekly_sched_name = weekly_sched_id2name[weekly_sched_id]
278
+
279
+ _add_sched_map(
280
+ sched_map, annual_sched_id, "Schedule:Week:Daily", weekly_sched_name
281
+ )
282
+
283
+ # get the weekly schedule row coincident with the weekly schedule id
284
+ (weekly_sched_row,) = (
285
+ row for row in weekly_sched_rows if row.ID == weekly_sched_id
286
+ )
287
+
288
+ for day_type in WEEKLY_SCHED_DAY_TYPES:
289
+ # get and add the daily schedule's ep object name
290
+ # to the `Schedule:Day:Hourly` set
291
+ daily_sched_id = getattr(weekly_sched_row, day_type)
292
+ daily_sched_name = daily_sched_id2name[daily_sched_id]
293
+
294
+ _add_sched_map(
295
+ sched_map, annual_sched_id, "Schedule:Day:Hourly", daily_sched_name
296
+ )
297
+
298
+ # get the daily schedule row coincident with the daily schedule id
299
+ (daily_sched_row,) = (
300
+ row for row in daily_sched_rows if row.ID == daily_sched_id
301
+ )
302
+
303
+ # get and add the daily schedule type's ep object name
304
+ daily_sched_type_name = sched_type_id2name[daily_sched_row.TYPE]
305
+ _add_sched_map(
306
+ sched_map,
307
+ annual_sched_id,
308
+ "ScheduleTypeLimits",
309
+ daily_sched_type_name,
310
+ )
311
+
312
+ # -----------------------------------------------------------------------------
313
+ # 3) convert annual, weekly and daily schedules to epJSON objects
314
+ # -----------------------------------------------------------------------------
315
+
316
+ epjson_objs: epJSONObjs = {}
317
+
318
+ # add epJSON `ScheduleTypeLimits` objects
319
+ for sched_type_name, epjson_obj_body in SCHED_TYPE_LIMITS_OBJS.items():
320
+ _add_epjson_obj(epjson_objs, sched_type_name, epjson_obj_body)
321
+
322
+ # convert annual schedules to epJSON `Schedule:Year` objects
323
+ for annual_sched_row in annual_sched_rows:
324
+ annual_sched_id = annual_sched_row.ID
325
+ annual_sched_name = annual_sched_id2name[annual_sched_id]
326
+
327
+ # create the list of schedule weeks given the annual schedule
328
+ # add placeholders for start month and start day
329
+ # sort the schedule weeks by end month and end day
330
+ sched_weeks = []
331
+
332
+ for annual_weekly_sched_row in annual_weekly_sched_rows:
333
+ if annual_weekly_sched_row.ANNUAL_SCHEDULE != annual_sched_id:
334
+ continue
335
+
336
+ sched_weeks.append(
337
+ {
338
+ "schedule_week_name": weekly_sched_id2name[
339
+ annual_weekly_sched_row.WEEKLY_SCHEDULE
340
+ ],
341
+ "start_month": -1,
342
+ "start_day": -1,
343
+ "end_month": MONTH_ABBR2NUM[annual_weekly_sched_row.END_MONTH],
344
+ "end_day": int(annual_weekly_sched_row.END_DAY),
345
+ }
346
+ )
347
+
348
+ sched_weeks.sort(key=lambda x: (x["end_month"], x["end_day"]))
349
+
350
+ # check that the last end month and end day are 12 and 31
351
+ assert sched_weeks[-1]["end_month"] == 12
352
+ assert sched_weeks[-1]["end_day"] == 31
353
+
354
+ # update start month and start day for each schedule week
355
+ # based on the end month and end day of the previous schedule week
356
+ for i, sched_week in enumerate(sched_weeks):
357
+ if i == 0:
358
+ start_month, start_day = 1, 1
359
+ else:
360
+ start_month, start_day = _next_month_day(
361
+ sched_weeks[i - 1]["end_month"], # type: ignore[arg-type]
362
+ sched_weeks[i - 1]["end_day"], # type: ignore[arg-type]
363
+ )
364
+ sched_week["start_month"] = start_month
365
+ sched_week["start_day"] = start_day
366
+
367
+ # get schedule type name
368
+ sched_type_name = sched_type_id2name[annual_sched_row.TYPE]
369
+
370
+ # create and add the epJSON `Schedule:Year` object
371
+ epjson_obj_body = {
372
+ "schedule_type_limits_name": sched_type_name,
373
+ "schedule_weeks": sched_weeks,
374
+ }
375
+ _add_epjson_obj(epjson_objs, annual_sched_name, epjson_obj_body)
376
+
377
+ # convert weekly schedules to epJSON `Schedule:Week:Daily` objects
378
+ for weekly_sched_row in weekly_sched_rows:
379
+ weekly_sched_name = weekly_sched_id2name[weekly_sched_row.ID]
380
+
381
+ # create and add the epJSON `Schedule:Week:Daily` object
382
+ epjson_obj_body = {
383
+ f"{day_type.lower()}_schedule_day_name": daily_sched_id2name[
384
+ getattr(weekly_sched_row, day_type)
385
+ ]
386
+ for day_type in WEEKLY_SCHED_DAY_TYPES
387
+ }
388
+ # TODO: customday1 and customday2 can probably be safely ignored
389
+ # but summerdesignday and winterdesignday need a better way to be handled
390
+ fallback_daily_sched_name = epjson_obj_body["holiday_schedule_day_name"]
391
+ epjson_obj_body |= {
392
+ "summerdesignday_schedule_day_name": fallback_daily_sched_name,
393
+ "winterdesignday_schedule_day_name": fallback_daily_sched_name,
394
+ "customday1_schedule_day_name": fallback_daily_sched_name,
395
+ "customday2_schedule_day_name": fallback_daily_sched_name,
396
+ }
397
+ _add_epjson_obj(epjson_objs, weekly_sched_name, epjson_obj_body)
398
+
399
+ # convert daily schedules to epJSON `Schedule:Day:Hourly` objects
400
+ for daily_sched_row in daily_sched_rows:
401
+ daily_sched_name = daily_sched_id2name[daily_sched_row.ID]
402
+
403
+ # get schedule type name
404
+ sched_type_name = sched_type_id2name[daily_sched_row.TYPE]
405
+
406
+ # create and add the epJSON `Schedule:Day:Hourly` object
407
+ epjson_obj_body = {"sched_type_limits_name": sched_type_name}
408
+ epjson_obj_body |= {
409
+ f"hour_{i + 1}": getattr(daily_sched_row, f"h{i:02d}") for i in range(24)
410
+ }
411
+ _add_epjson_obj(epjson_objs, daily_sched_name, epjson_obj_body)
412
+
413
+ return sched_map, epjson_objs
414
+
415
+
416
+ def pick_scheds(
417
+ room_names: Sequence[str],
418
+ sched_categories: Sequence[str],
419
+ sched_map: SchedMap,
420
+ epjson_objs: epJSONObjs,
421
+ activity_rows: PyODBCRows,
422
+ ) -> dict[str, epJSONObjs]:
423
+ """Pick epJSON schedule objects given rooms and schedule categories.
424
+
425
+ Parameters
426
+ ----------
427
+ room_names : Sequence[str]
428
+ NCM room names of interest.
429
+ sched_categories : Sequence[str]
430
+ NCM schedule categories of interest.
431
+ sched_map : SchedMap
432
+ Map of annual schedule ids to epJSON object names grouped by EP object type.
433
+ epjson_objs : epJSONObjs
434
+ Map of epJSON object names to epJSON object bodies.
435
+ activity_rows : PyODBCRows
436
+ Rows of the `[activity]` table.
437
+
438
+ Returns
439
+ -------
440
+ dict[str, epJSONObjs]
441
+ An epJSON of schedules.
442
+ """
443
+ annual_sched_ids = {
444
+ getattr(row, column_name)
445
+ for row in activity_rows
446
+ if row.NAME in room_names
447
+ for column_name in sched_categories
448
+ }
449
+
450
+ sched_epjson: dict[str, epJSONObjs] = {
451
+ "ScheduleTypeLimits": {},
452
+ "Schedule:Day:Hourly": {},
453
+ "Schedule:Week:Daily": {},
454
+ "Schedule:Year": {},
455
+ }
456
+
457
+ for annual_sched_id in annual_sched_ids:
458
+ for ep_obj_type, ep_obj_names in sched_map[annual_sched_id].items():
459
+ for ep_obj_name in sorted(ep_obj_names):
460
+ sched_epjson[ep_obj_type][ep_obj_name] = epjson_objs[ep_obj_name]
461
+
462
+ return sched_epjson
463
+
464
+
465
+ def get_scheds(
466
+ room_names: Sequence[str], sched_categories: Sequence[str], cursor: PyODBCCursor
467
+ ) -> dict[str, epJSONObjs]:
468
+ """Get an epJSON of schedules given rooms and schedule categories.
469
+
470
+ This is a helper function that streamlines `read_scheds`, `convert_scheds` and
471
+ `pick_scheds` into a single function call.
472
+
473
+ Parameters
474
+ ----------
475
+ room_names : Sequence[str]
476
+ NCM room names of interest.
477
+ sched_categories : Sequence[str]
478
+ NCM schedule category column names of interest.
479
+ cursor : PyODBCCursor
480
+ Open cursor of the NCM activity database.
481
+
482
+ Returns
483
+ -------
484
+ dict[str, epJSONObjs]
485
+ An epJSON of schedules.
486
+ """
487
+ # get activity rows
488
+ activity_rows = _fetch_filter("activity", "NAME", room_names, cursor)
489
+
490
+ # read NCM schedule data
491
+ (
492
+ annual_sched_rows,
493
+ annual_weekly_sched_rows,
494
+ weekly_sched_rows,
495
+ daily_sched_rows,
496
+ sched_type_rows,
497
+ ) = read_scheds(activity_rows, cursor)
498
+
499
+ # convert NCM schedule data into an epJSON schedule library
500
+ sched_map, epjson_objs = convert_scheds(
501
+ annual_sched_rows,
502
+ annual_weekly_sched_rows,
503
+ weekly_sched_rows,
504
+ daily_sched_rows,
505
+ sched_type_rows,
506
+ )
507
+
508
+ # pick epJSON schedule objects given rooms and schedule categories
509
+ return pick_scheds(
510
+ room_names, sched_categories, sched_map, epjson_objs, activity_rows
511
+ )
@@ -0,0 +1,532 @@
1
+ Metadata-Version: 2.4
2
+ Name: aark
3
+ Version: 0.1.0
4
+ Summary: airallergy's research kit
5
+ Project-URL: repository, https://github.com/airallergy/aark
6
+ Author-email: Cheng Cui <cheng.cui.95@gmail.com>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.14
19
+ Requires-Dist: pyodbc==5.*
20
+ Description-Content-Type: text/markdown
21
+
22
+ # `aark`
23
+
24
+ airallergy’s research kit
25
+
26
+ `aark` is a collection of Python tools for my research in built environment.
27
+
28
+ **Table of contents**
29
+
30
+ - [Usage](#usage)
31
+ - [NCM](#ncm)
32
+ - [Get activity schedules in the epJSON format](#get-activity-schedules-in-the-epjson-format)
33
+
34
+ ## Usage
35
+
36
+ ### NCM
37
+
38
+ Tools for analysing the [National Calculation Methodology (NCM) database](https://www.uk-ncm.org.uk/), tested primarily on data related to English dwellings.
39
+
40
+ > [!TIP]
41
+ > To access the NCM database, the NCM modules depend on `pyodbc`, which in turn depends on an ODBC driver and driver manager for Microsoft Access. See [`pyodbc`'s installation guide](https://github.com/mkleehammer/pyodbc/wiki/Install) for details.
42
+
43
+ #### Get activity schedules in the epJSON format
44
+
45
+ The `aark.ncm.sched` module provides functions for extracting NCM activity schedules and converting them into the epJSON format for EnergyPlus. In particular, the `aark.ncm.sched.get_scheds` function takes a sequence of NCM room names, a sequence of NCM schedule categories and a `pyodbc.Cursor` object for the NCM activity database, and returns an epJSON of schedule objects ready for EnergyPlus simulations.
46
+
47
+ > [!IMPORTANT]
48
+ > This module currently has the following caveats:
49
+ >
50
+ > - A non-leap year is assumed.
51
+ > - Other gains schedules are not converted.
52
+
53
+ For instance, the following script retrieves the occupancy and heating set point schedules for the bedroom and the lounge.
54
+
55
+ ```python
56
+ import pyodbc
57
+
58
+ import aark.ncm.sched
59
+
60
+
61
+ room_names = ["Dwell_DomBed", "Dwell_DomLounge"]
62
+ sched_categories = ["OCCUPANCY_SCH", "HEAT_SET_SCH"]
63
+
64
+ with pyodbc.connect(
65
+ Driver="Microsoft Access Driver (*.mdb, *.accdb)", DBQ="/path/to/the/database"
66
+ ) as con:
67
+ cur = con.cursor()
68
+
69
+ sched_epjson = aark.ncm.sched.get_scheds(
70
+ room_names=room_names, sched_categories=sched_categories, cursor=cur
71
+ )
72
+ ```
73
+
74
+ The `sched_epjson` variable contains the following epJSON of schedule objects.
75
+
76
+ <details>
77
+ <summary>Full epJSON</summary>
78
+
79
+ ```json
80
+ {
81
+ "ScheduleTypeLimits": {
82
+ "TEMPERATURE": {
83
+ "lower_limit_value": -100,
84
+ "upper_limit_value": 100,
85
+ "numeric_type": "Continuous",
86
+ "unit_type": "Temperature"
87
+ },
88
+ "FRACTION": {
89
+ "lower_limit_value": 0,
90
+ "upper_limit_value": 1,
91
+ "numeric_type": "Continuous",
92
+ "unit_type": "Dimensionless"
93
+ }
94
+ },
95
+ "Schedule:Day:Hourly": {
96
+ "ncm-daily-9344-Dwell_DomBed_Heat_Wkdy": {
97
+ "sched_type_limits_name": "TEMPERATURE",
98
+ "hour_1": 18,
99
+ "hour_2": 18,
100
+ "hour_3": 18,
101
+ "hour_4": 18,
102
+ "hour_5": 18,
103
+ "hour_6": 18,
104
+ "hour_7": 18,
105
+ "hour_8": 18,
106
+ "hour_9": 18,
107
+ "hour_10": 12,
108
+ "hour_11": 12,
109
+ "hour_12": 12,
110
+ "hour_13": 12,
111
+ "hour_14": 12,
112
+ "hour_15": 12,
113
+ "hour_16": 12,
114
+ "hour_17": 12,
115
+ "hour_18": 12,
116
+ "hour_19": 12,
117
+ "hour_20": 12,
118
+ "hour_21": 18,
119
+ "hour_22": 18,
120
+ "hour_23": 18,
121
+ "hour_24": 18
122
+ },
123
+ "ncm-daily-9345-Dwell_DomBed_Heat_Wknd": {
124
+ "sched_type_limits_name": "TEMPERATURE",
125
+ "hour_1": 18,
126
+ "hour_2": 18,
127
+ "hour_3": 18,
128
+ "hour_4": 18,
129
+ "hour_5": 18,
130
+ "hour_6": 18,
131
+ "hour_7": 18,
132
+ "hour_8": 18,
133
+ "hour_9": 18,
134
+ "hour_10": 12,
135
+ "hour_11": 12,
136
+ "hour_12": 12,
137
+ "hour_13": 12,
138
+ "hour_14": 12,
139
+ "hour_15": 12,
140
+ "hour_16": 12,
141
+ "hour_17": 12,
142
+ "hour_18": 12,
143
+ "hour_19": 12,
144
+ "hour_20": 12,
145
+ "hour_21": 18,
146
+ "hour_22": 18,
147
+ "hour_23": 18,
148
+ "hour_24": 18
149
+ },
150
+ "ncm-daily-9346-Dwell_DomBed_Heat_Hol": {
151
+ "sched_type_limits_name": "TEMPERATURE",
152
+ "hour_1": 18,
153
+ "hour_2": 18,
154
+ "hour_3": 18,
155
+ "hour_4": 18,
156
+ "hour_5": 18,
157
+ "hour_6": 18,
158
+ "hour_7": 18,
159
+ "hour_8": 18,
160
+ "hour_9": 18,
161
+ "hour_10": 12,
162
+ "hour_11": 12,
163
+ "hour_12": 12,
164
+ "hour_13": 12,
165
+ "hour_14": 12,
166
+ "hour_15": 12,
167
+ "hour_16": 12,
168
+ "hour_17": 12,
169
+ "hour_18": 12,
170
+ "hour_19": 12,
171
+ "hour_20": 12,
172
+ "hour_21": 18,
173
+ "hour_22": 18,
174
+ "hour_23": 18,
175
+ "hour_24": 18
176
+ },
177
+ "ncm-daily-9395-Dwell_DomLounge_Occ_Wkdy": {
178
+ "sched_type_limits_name": "FRACTION",
179
+ "hour_1": 0,
180
+ "hour_2": 0,
181
+ "hour_3": 0,
182
+ "hour_4": 0,
183
+ "hour_5": 0,
184
+ "hour_6": 0,
185
+ "hour_7": 0,
186
+ "hour_8": 0,
187
+ "hour_9": 0,
188
+ "hour_10": 0,
189
+ "hour_11": 0,
190
+ "hour_12": 0,
191
+ "hour_13": 0,
192
+ "hour_14": 0,
193
+ "hour_15": 0,
194
+ "hour_16": 0,
195
+ "hour_17": 0.5,
196
+ "hour_18": 0.5,
197
+ "hour_19": 1,
198
+ "hour_20": 1,
199
+ "hour_21": 1,
200
+ "hour_22": 1,
201
+ "hour_23": 0.666666667,
202
+ "hour_24": 0
203
+ },
204
+ "ncm-daily-9396-Dwell_DomLounge_Occ_Wknd": {
205
+ "sched_type_limits_name": "FRACTION",
206
+ "hour_1": 0,
207
+ "hour_2": 0,
208
+ "hour_3": 0,
209
+ "hour_4": 0,
210
+ "hour_5": 0,
211
+ "hour_6": 0,
212
+ "hour_7": 0,
213
+ "hour_8": 0,
214
+ "hour_9": 0,
215
+ "hour_10": 0,
216
+ "hour_11": 0,
217
+ "hour_12": 0,
218
+ "hour_13": 0,
219
+ "hour_14": 0,
220
+ "hour_15": 0,
221
+ "hour_16": 0,
222
+ "hour_17": 0.5,
223
+ "hour_18": 0.5,
224
+ "hour_19": 1,
225
+ "hour_20": 1,
226
+ "hour_21": 1,
227
+ "hour_22": 1,
228
+ "hour_23": 0.666666667,
229
+ "hour_24": 0
230
+ },
231
+ "ncm-daily-9397-Dwell_DomLounge_Occ_Hol": {
232
+ "sched_type_limits_name": "FRACTION",
233
+ "hour_1": 0,
234
+ "hour_2": 0,
235
+ "hour_3": 0,
236
+ "hour_4": 0,
237
+ "hour_5": 0,
238
+ "hour_6": 0,
239
+ "hour_7": 0,
240
+ "hour_8": 0,
241
+ "hour_9": 0,
242
+ "hour_10": 0,
243
+ "hour_11": 0,
244
+ "hour_12": 0,
245
+ "hour_13": 0,
246
+ "hour_14": 0,
247
+ "hour_15": 0,
248
+ "hour_16": 0,
249
+ "hour_17": 0.5,
250
+ "hour_18": 0.5,
251
+ "hour_19": 1,
252
+ "hour_20": 1,
253
+ "hour_21": 1,
254
+ "hour_22": 1,
255
+ "hour_23": 0.666666667,
256
+ "hour_24": 0
257
+ },
258
+ "ncm-daily-9404-Dwell_DomLounge_Heat_Wkdy": {
259
+ "sched_type_limits_name": "TEMPERATURE",
260
+ "hour_1": 12,
261
+ "hour_2": 12,
262
+ "hour_3": 12,
263
+ "hour_4": 12,
264
+ "hour_5": 12,
265
+ "hour_6": 12,
266
+ "hour_7": 12,
267
+ "hour_8": 12,
268
+ "hour_9": 12,
269
+ "hour_10": 12,
270
+ "hour_11": 12,
271
+ "hour_12": 12,
272
+ "hour_13": 12,
273
+ "hour_14": 12,
274
+ "hour_15": 21,
275
+ "hour_16": 21,
276
+ "hour_17": 21,
277
+ "hour_18": 21,
278
+ "hour_19": 21,
279
+ "hour_20": 21,
280
+ "hour_21": 21,
281
+ "hour_22": 21,
282
+ "hour_23": 21,
283
+ "hour_24": 12
284
+ },
285
+ "ncm-daily-9405-Dwell_DomLounge_Heat_Wknd": {
286
+ "sched_type_limits_name": "TEMPERATURE",
287
+ "hour_1": 12,
288
+ "hour_2": 12,
289
+ "hour_3": 12,
290
+ "hour_4": 12,
291
+ "hour_5": 12,
292
+ "hour_6": 12,
293
+ "hour_7": 12,
294
+ "hour_8": 12,
295
+ "hour_9": 12,
296
+ "hour_10": 12,
297
+ "hour_11": 12,
298
+ "hour_12": 12,
299
+ "hour_13": 12,
300
+ "hour_14": 12,
301
+ "hour_15": 21,
302
+ "hour_16": 21,
303
+ "hour_17": 21,
304
+ "hour_18": 21,
305
+ "hour_19": 21,
306
+ "hour_20": 21,
307
+ "hour_21": 21,
308
+ "hour_22": 21,
309
+ "hour_23": 21,
310
+ "hour_24": 12
311
+ },
312
+ "ncm-daily-9406-Dwell_DomLounge_Heat_Hol": {
313
+ "sched_type_limits_name": "TEMPERATURE",
314
+ "hour_1": 12,
315
+ "hour_2": 12,
316
+ "hour_3": 12,
317
+ "hour_4": 12,
318
+ "hour_5": 12,
319
+ "hour_6": 12,
320
+ "hour_7": 12,
321
+ "hour_8": 12,
322
+ "hour_9": 12,
323
+ "hour_10": 12,
324
+ "hour_11": 12,
325
+ "hour_12": 12,
326
+ "hour_13": 12,
327
+ "hour_14": 12,
328
+ "hour_15": 21,
329
+ "hour_16": 21,
330
+ "hour_17": 21,
331
+ "hour_18": 21,
332
+ "hour_19": 21,
333
+ "hour_20": 21,
334
+ "hour_21": 21,
335
+ "hour_22": 21,
336
+ "hour_23": 21,
337
+ "hour_24": 12
338
+ },
339
+ "ncm-daily-9335-Dwell_DomBed_Occ_Wkdy": {
340
+ "sched_type_limits_name": "FRACTION",
341
+ "hour_1": 1,
342
+ "hour_2": 1,
343
+ "hour_3": 1,
344
+ "hour_4": 1,
345
+ "hour_5": 1,
346
+ "hour_6": 1,
347
+ "hour_7": 1,
348
+ "hour_8": 0.5,
349
+ "hour_9": 0.25,
350
+ "hour_10": 0,
351
+ "hour_11": 0,
352
+ "hour_12": 0,
353
+ "hour_13": 0,
354
+ "hour_14": 0,
355
+ "hour_15": 0,
356
+ "hour_16": 0,
357
+ "hour_17": 0,
358
+ "hour_18": 0,
359
+ "hour_19": 0,
360
+ "hour_20": 0,
361
+ "hour_21": 0,
362
+ "hour_22": 0,
363
+ "hour_23": 0.25,
364
+ "hour_24": 0.75
365
+ },
366
+ "ncm-daily-9336-Dwell_DomBed_Occ_Wknd": {
367
+ "sched_type_limits_name": "FRACTION",
368
+ "hour_1": 1,
369
+ "hour_2": 1,
370
+ "hour_3": 1,
371
+ "hour_4": 1,
372
+ "hour_5": 1,
373
+ "hour_6": 1,
374
+ "hour_7": 1,
375
+ "hour_8": 0.5,
376
+ "hour_9": 0.25,
377
+ "hour_10": 0,
378
+ "hour_11": 0,
379
+ "hour_12": 0,
380
+ "hour_13": 0,
381
+ "hour_14": 0,
382
+ "hour_15": 0,
383
+ "hour_16": 0,
384
+ "hour_17": 0,
385
+ "hour_18": 0,
386
+ "hour_19": 0,
387
+ "hour_20": 0,
388
+ "hour_21": 0,
389
+ "hour_22": 0,
390
+ "hour_23": 0.25,
391
+ "hour_24": 0.75
392
+ },
393
+ "ncm-daily-9337-Dwell_DomBed_Occ_Hol": {
394
+ "sched_type_limits_name": "FRACTION",
395
+ "hour_1": 1,
396
+ "hour_2": 1,
397
+ "hour_3": 1,
398
+ "hour_4": 1,
399
+ "hour_5": 1,
400
+ "hour_6": 1,
401
+ "hour_7": 1,
402
+ "hour_8": 0.5,
403
+ "hour_9": 0.25,
404
+ "hour_10": 0,
405
+ "hour_11": 0,
406
+ "hour_12": 0,
407
+ "hour_13": 0,
408
+ "hour_14": 0,
409
+ "hour_15": 0,
410
+ "hour_16": 0,
411
+ "hour_17": 0,
412
+ "hour_18": 0,
413
+ "hour_19": 0,
414
+ "hour_20": 0,
415
+ "hour_21": 0,
416
+ "hour_22": 0,
417
+ "hour_23": 0.25,
418
+ "hour_24": 0.75
419
+ }
420
+ },
421
+ "Schedule:Week:Daily": {
422
+ "ncm-weekly-3530-Dwell_DomBed_Heat_Wk1": {
423
+ "monday_schedule_day_name": "ncm-daily-9344-Dwell_DomBed_Heat_Wkdy",
424
+ "tuesday_schedule_day_name": "ncm-daily-9344-Dwell_DomBed_Heat_Wkdy",
425
+ "wednesday_schedule_day_name": "ncm-daily-9344-Dwell_DomBed_Heat_Wkdy",
426
+ "thursday_schedule_day_name": "ncm-daily-9344-Dwell_DomBed_Heat_Wkdy",
427
+ "friday_schedule_day_name": "ncm-daily-9344-Dwell_DomBed_Heat_Wkdy",
428
+ "saturday_schedule_day_name": "ncm-daily-9345-Dwell_DomBed_Heat_Wknd",
429
+ "sunday_schedule_day_name": "ncm-daily-9345-Dwell_DomBed_Heat_Wknd",
430
+ "holiday_schedule_day_name": "ncm-daily-9346-Dwell_DomBed_Heat_Hol",
431
+ "summerdesignday_schedule_day_name": "ncm-daily-9346-Dwell_DomBed_Heat_Hol",
432
+ "winterdesignday_schedule_day_name": "ncm-daily-9346-Dwell_DomBed_Heat_Hol",
433
+ "customday1_schedule_day_name": "ncm-daily-9346-Dwell_DomBed_Heat_Hol",
434
+ "customday2_schedule_day_name": "ncm-daily-9346-Dwell_DomBed_Heat_Hol"
435
+ },
436
+ "ncm-weekly-3557-Dwell_DomLounge_Occ_Wk1": {
437
+ "monday_schedule_day_name": "ncm-daily-9395-Dwell_DomLounge_Occ_Wkdy",
438
+ "tuesday_schedule_day_name": "ncm-daily-9395-Dwell_DomLounge_Occ_Wkdy",
439
+ "wednesday_schedule_day_name": "ncm-daily-9395-Dwell_DomLounge_Occ_Wkdy",
440
+ "thursday_schedule_day_name": "ncm-daily-9395-Dwell_DomLounge_Occ_Wkdy",
441
+ "friday_schedule_day_name": "ncm-daily-9395-Dwell_DomLounge_Occ_Wkdy",
442
+ "saturday_schedule_day_name": "ncm-daily-9396-Dwell_DomLounge_Occ_Wknd",
443
+ "sunday_schedule_day_name": "ncm-daily-9396-Dwell_DomLounge_Occ_Wknd",
444
+ "holiday_schedule_day_name": "ncm-daily-9397-Dwell_DomLounge_Occ_Hol",
445
+ "summerdesignday_schedule_day_name": "ncm-daily-9397-Dwell_DomLounge_Occ_Hol",
446
+ "winterdesignday_schedule_day_name": "ncm-daily-9397-Dwell_DomLounge_Occ_Hol",
447
+ "customday1_schedule_day_name": "ncm-daily-9397-Dwell_DomLounge_Occ_Hol",
448
+ "customday2_schedule_day_name": "ncm-daily-9397-Dwell_DomLounge_Occ_Hol"
449
+ },
450
+ "ncm-weekly-3555-Dwell_DomLounge_Heat_Wk1": {
451
+ "monday_schedule_day_name": "ncm-daily-9404-Dwell_DomLounge_Heat_Wkdy",
452
+ "tuesday_schedule_day_name": "ncm-daily-9404-Dwell_DomLounge_Heat_Wkdy",
453
+ "wednesday_schedule_day_name": "ncm-daily-9404-Dwell_DomLounge_Heat_Wkdy",
454
+ "thursday_schedule_day_name": "ncm-daily-9404-Dwell_DomLounge_Heat_Wkdy",
455
+ "friday_schedule_day_name": "ncm-daily-9404-Dwell_DomLounge_Heat_Wkdy",
456
+ "saturday_schedule_day_name": "ncm-daily-9405-Dwell_DomLounge_Heat_Wknd",
457
+ "sunday_schedule_day_name": "ncm-daily-9405-Dwell_DomLounge_Heat_Wknd",
458
+ "holiday_schedule_day_name": "ncm-daily-9406-Dwell_DomLounge_Heat_Hol",
459
+ "summerdesignday_schedule_day_name": "ncm-daily-9406-Dwell_DomLounge_Heat_Hol",
460
+ "winterdesignday_schedule_day_name": "ncm-daily-9406-Dwell_DomLounge_Heat_Hol",
461
+ "customday1_schedule_day_name": "ncm-daily-9406-Dwell_DomLounge_Heat_Hol",
462
+ "customday2_schedule_day_name": "ncm-daily-9406-Dwell_DomLounge_Heat_Hol"
463
+ },
464
+ "ncm-weekly-3532-Dwell_DomBed_Occ_Wk1": {
465
+ "monday_schedule_day_name": "ncm-daily-9335-Dwell_DomBed_Occ_Wkdy",
466
+ "tuesday_schedule_day_name": "ncm-daily-9335-Dwell_DomBed_Occ_Wkdy",
467
+ "wednesday_schedule_day_name": "ncm-daily-9335-Dwell_DomBed_Occ_Wkdy",
468
+ "thursday_schedule_day_name": "ncm-daily-9335-Dwell_DomBed_Occ_Wkdy",
469
+ "friday_schedule_day_name": "ncm-daily-9335-Dwell_DomBed_Occ_Wkdy",
470
+ "saturday_schedule_day_name": "ncm-daily-9336-Dwell_DomBed_Occ_Wknd",
471
+ "sunday_schedule_day_name": "ncm-daily-9336-Dwell_DomBed_Occ_Wknd",
472
+ "holiday_schedule_day_name": "ncm-daily-9337-Dwell_DomBed_Occ_Hol",
473
+ "summerdesignday_schedule_day_name": "ncm-daily-9337-Dwell_DomBed_Occ_Hol",
474
+ "winterdesignday_schedule_day_name": "ncm-daily-9337-Dwell_DomBed_Occ_Hol",
475
+ "customday1_schedule_day_name": "ncm-daily-9337-Dwell_DomBed_Occ_Hol",
476
+ "customday2_schedule_day_name": "ncm-daily-9337-Dwell_DomBed_Occ_Hol"
477
+ }
478
+ },
479
+ "Schedule:Year": {
480
+ "ncm-annual-3232-Dwell_DomBed_Heat": {
481
+ "schedule_type_limits_name": "TEMPERATURE",
482
+ "schedule_weeks": [
483
+ {
484
+ "schedule_week_name": "ncm-weekly-3530-Dwell_DomBed_Heat_Wk1",
485
+ "start_month": 1,
486
+ "start_day": 1,
487
+ "end_month": 12,
488
+ "end_day": 31
489
+ }
490
+ ]
491
+ },
492
+ "ncm-annual-3249-Dwell_DomLounge_Occ": {
493
+ "schedule_type_limits_name": "FRACTION",
494
+ "schedule_weeks": [
495
+ {
496
+ "schedule_week_name": "ncm-weekly-3557-Dwell_DomLounge_Occ_Wk1",
497
+ "start_month": 1,
498
+ "start_day": 1,
499
+ "end_month": 12,
500
+ "end_day": 31
501
+ }
502
+ ]
503
+ },
504
+ "ncm-annual-3252-Dwell_DomLounge_Heat": {
505
+ "schedule_type_limits_name": "TEMPERATURE",
506
+ "schedule_weeks": [
507
+ {
508
+ "schedule_week_name": "ncm-weekly-3555-Dwell_DomLounge_Heat_Wk1",
509
+ "start_month": 1,
510
+ "start_day": 1,
511
+ "end_month": 12,
512
+ "end_day": 31
513
+ }
514
+ ]
515
+ },
516
+ "ncm-annual-3229-Dwell_DomBed_Occ": {
517
+ "schedule_type_limits_name": "FRACTION",
518
+ "schedule_weeks": [
519
+ {
520
+ "schedule_week_name": "ncm-weekly-3532-Dwell_DomBed_Occ_Wk1",
521
+ "start_month": 1,
522
+ "start_day": 1,
523
+ "end_month": 12,
524
+ "end_day": 31
525
+ }
526
+ ]
527
+ }
528
+ }
529
+ }
530
+ ```
531
+
532
+ </details>
@@ -0,0 +1,7 @@
1
+ aark/__init__.py,sha256=6Rym56FY3h16K5RGHbFuOtI2FZPdVuxAZtJu2D7HlF4,56
2
+ aark/ncm/__init__.py,sha256=N2HgG0qMbj8bMWN7oxT2LzVa35awcaLHFnsyif_PwIo,74
3
+ aark/ncm/sched.py,sha256=_wQ75iNo_H7Aid0UjcZjPNS5bkSe4z7Yf2UslRWJxec,17610
4
+ aark-0.1.0.dist-info/METADATA,sha256=uB9s9G8xVIOlIwSj7gb0JpFXJA4BN9GOpOqdWcYZrko,15554
5
+ aark-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ aark-0.1.0.dist-info/licenses/LICENSE,sha256=JTVcXvF_ZZiquXM-HlNz4fk7GpbNzHhVkzrMBK5iqzY,1496
7
+ aark-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Cheng Cui
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.