pyfemtet 1.0.5__py3-none-any.whl → 1.0.7__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.

Potentially problematic release.


This version of pyfemtet might be problematic. Click here for more details.

pyfemtet/opt/femopt.py CHANGED
@@ -157,6 +157,15 @@ class FEMOpt:
157
157
  ):
158
158
  self.opt.add_constraint(name, fun, lower_bound, upper_bound, args, kwargs, strict, using_fem)
159
159
 
160
+ def add_other_output(
161
+ self,
162
+ name: str,
163
+ fun: Callable[..., float],
164
+ args: tuple | None = None,
165
+ kwargs: dict | None = None,
166
+ ):
167
+ self.opt.add_other_output(name, fun, args, kwargs)
168
+
160
169
  def add_sub_fidelity_model(
161
170
  self,
162
171
  name: str,
@@ -326,6 +326,7 @@ class ColumnManager:
326
326
  parameters: TrialInput
327
327
  y_names: list[str]
328
328
  c_names: list[str]
329
+ other_output_names: list[str]
329
330
  column_dtypes: dict[str, type]
330
331
  meta_columns: list[str]
331
332
 
@@ -340,12 +341,14 @@ class ColumnManager:
340
341
  parameters: TrialInput,
341
342
  y_names,
342
343
  c_names,
344
+ other_output_names,
343
345
  additional_data: dict,
344
346
  column_order_mode: str = ColumnOrderMode.per_category,
345
347
  ):
346
348
  self.parameters = parameters
347
349
  self.y_names = y_names
348
350
  self.c_names = c_names
351
+ self.other_output_names=other_output_names
349
352
  self.set_full_sorted_column_information(
350
353
  additional_data=additional_data,
351
354
  column_order_mode=column_order_mode,
@@ -356,12 +359,14 @@ class ColumnManager:
356
359
  extra_parameters: TrialInput = None,
357
360
  extra_y_names: list[str] = None,
358
361
  extra_c_names: list[str] = None,
362
+ extra_other_output_names: list[str] = None,
359
363
  additional_data: dict = None,
360
364
  column_order_mode: str = ColumnOrderMode.per_category,
361
365
  ):
362
366
  extra_parameters = extra_parameters or TrialInput()
363
367
  extra_y_names = extra_y_names or []
364
368
  extra_c_names = extra_c_names or []
369
+ extra_other_output_names = extra_other_output_names or []
365
370
 
366
371
  # column name になるので重複は許されない
367
372
  column_dtypes: dict = NoDuplicateDict()
@@ -503,6 +508,17 @@ class ColumnManager:
503
508
  target_cds.update({f_ub(name): float})
504
509
  target_mcs.append('')
505
510
 
511
+ elif key == 'other_outputs':
512
+ for name in self.other_output_names:
513
+ # important
514
+ column_dtypes.update({name: float})
515
+ meta_columns.append('other_output.value')
516
+
517
+ for name in extra_other_output_names:
518
+ # later
519
+ target_cds.update({name: float})
520
+ target_mcs.append('')
521
+
506
522
  # additional_data を入れる
507
523
  elif key == self._get_additional_data_column():
508
524
  # important
@@ -578,6 +594,9 @@ class ColumnManager:
578
594
  def get_cns_names(self) -> list[str]:
579
595
  return self.filter_columns('cns')
580
596
 
597
+ def get_other_output_names(self) -> list[str]:
598
+ return self.filter_columns('other_output')
599
+
581
600
  @staticmethod
582
601
  def _is_numerical_parameter(prm_name, columns):
583
602
  prm_lb_name = CorrespondingColumnNameRuler.prm_lower_bound_name(prm_name)
@@ -672,6 +691,7 @@ class Record:
672
691
  x: TrialInput = dataclasses.field(default_factory=TrialInput)
673
692
  y: TrialOutput = dataclasses.field(default_factory=TrialOutput)
674
693
  c: TrialConstraintOutput = dataclasses.field(default_factory=TrialConstraintOutput)
694
+ other_outputs: TrialFunctionOutput = dataclasses.field(default_factory=TrialFunctionOutput)
675
695
  state: TrialState = TrialState.undefined
676
696
  datetime_start: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now)
677
697
  datetime_end: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now)
@@ -689,6 +709,7 @@ class Record:
689
709
  x: TrialInput = d.pop('x')
690
710
  y: TrialOutput = d.pop('y')
691
711
  c: TrialConstraintOutput = d.pop('c')
712
+ other_outputs: TrialFunctionOutput = d.pop('other_outputs')
692
713
 
693
714
  # prm
694
715
  for prm_name, param in x.items():
@@ -723,6 +744,9 @@ class Record:
723
744
  d.update(**{f'{f_ub(k)}': v.upper_bound
724
745
  for k, v in c.items()})
725
746
 
747
+ # function
748
+ d.update(**{k: v.value for k, v in other_outputs.items()})
749
+
726
750
  df = pd.DataFrame(
727
751
  {k: [v] for k, v in d.items()},
728
752
  columns=tuple(dtypes.keys())
@@ -917,11 +941,13 @@ class Records:
917
941
  loaded_prm_names = set(self.column_manager._filter_prm_names(loaded_columns, loaded_meta_columns))
918
942
  loaded_obj_names = set(self.column_manager._filter_columns('obj', loaded_columns, loaded_meta_columns))
919
943
  loaded_cns_names = set(self.column_manager._filter_columns('cns', loaded_columns, loaded_meta_columns))
944
+ loaded_other_output_names = set(self.column_manager._filter_columns('other_output.value', loaded_columns, loaded_meta_columns))
920
945
 
921
946
  # loaded df に存在するが Record に存在しないカラムを Record に追加
922
947
  extra_parameters = {}
923
948
  extra_y_names = []
924
949
  extra_c_names = []
950
+ extra_oo_names = []
925
951
  for l_col, l_meta in zip(loaded_columns, loaded_meta_columns):
926
952
 
927
953
  # 現在の Record に含まれないならば
@@ -958,6 +984,10 @@ class Records:
958
984
  elif l_col in loaded_cns_names:
959
985
  extra_c_names.append(l_col)
960
986
 
987
+ # other_output_name ならば
988
+ elif l_col in loaded_other_output_names:
989
+ extra_oo_names.append(l_col)
990
+
961
991
  # additional data を取得
962
992
  a_data = self.column_manager._get_additional_data(loaded_columns, loaded_meta_columns)
963
993
 
@@ -965,6 +995,7 @@ class Records:
965
995
  extra_parameters=extra_parameters,
966
996
  extra_y_names=extra_y_names,
967
997
  extra_c_names=extra_c_names,
998
+ extra_other_output_names=extra_oo_names,
968
999
  additional_data=a_data,
969
1000
  column_order_mode=column_order_mode,
970
1001
  )
@@ -1139,6 +1170,7 @@ class History:
1139
1170
  prm_names: list[str]
1140
1171
  obj_names: list[str]
1141
1172
  cns_names: list[str]
1173
+ other_output_names: list[str]
1142
1174
  sub_fidelity_names: list[str]
1143
1175
  is_restart: bool
1144
1176
  additional_data: dict
@@ -1151,6 +1183,10 @@ class History:
1151
1183
  when the optimization process starts.
1152
1184
  """
1153
1185
 
1186
+ @property
1187
+ def all_output_names(self) -> list[str]:
1188
+ return self.obj_names + self.cns_names + self.other_output_names
1189
+
1154
1190
  def __init__(self):
1155
1191
  self._records = Records()
1156
1192
  self.path: str | None = None
@@ -1189,6 +1225,7 @@ class History:
1189
1225
  self.prm_names = ColumnManager._filter_prm_names(df.columns, meta_columns)
1190
1226
  self.obj_names = ColumnManager._filter_columns('obj', df.columns, meta_columns)
1191
1227
  self.cns_names = ColumnManager._filter_columns('cns', df.columns, meta_columns)
1228
+ self.other_output_names = ColumnManager._filter_columns('other_output.value', df.columns, meta_columns)
1192
1229
  self.sub_fidelity_names = ColumnManager._get_sub_fidelity_names(df)
1193
1230
  self.additional_data = ColumnManager._get_additional_data(df.columns, meta_columns)
1194
1231
 
@@ -1201,6 +1238,7 @@ class History:
1201
1238
  parameters,
1202
1239
  self.obj_names,
1203
1240
  self.cns_names,
1241
+ self.other_output_names,
1204
1242
  self.sub_fidelity_names,
1205
1243
  self.additional_data,
1206
1244
  )
@@ -1210,6 +1248,7 @@ class History:
1210
1248
  parameters: TrialInput,
1211
1249
  obj_names,
1212
1250
  cns_names,
1251
+ other_output_names,
1213
1252
  sub_fidelity_names,
1214
1253
  additional_data,
1215
1254
  ):
@@ -1218,13 +1257,15 @@ class History:
1218
1257
  self.prm_names = list(parameters.keys())
1219
1258
  self.obj_names = list(obj_names)
1220
1259
  self.cns_names = list(cns_names)
1260
+ self.other_output_names = list(other_output_names)
1221
1261
  self.sub_fidelity_names = list(sub_fidelity_names)
1222
1262
  self.additional_data.update(additional_data)
1223
1263
 
1224
1264
  if not self._finalized:
1225
1265
  # ここで column_dtypes が決定する
1226
1266
  self._records.column_manager.initialize(
1227
- parameters, self.obj_names, self.cns_names, self.additional_data, self.column_order_mode
1267
+ parameters, self.obj_names, self.cns_names, self.other_output_names,
1268
+ self.additional_data, self.column_order_mode
1228
1269
  )
1229
1270
 
1230
1271
  # initialize
@@ -1381,6 +1422,8 @@ class History:
1381
1422
  self._records.save(self.path)
1382
1423
 
1383
1424
  def _create_optuna_study_for_visualization(self):
1425
+ """出力は internal ではない値で、objective は出力という意味であり cns, other_output を含む。"""
1426
+
1384
1427
  import optuna
1385
1428
 
1386
1429
  # create study
@@ -1388,10 +1431,10 @@ class History:
1388
1431
  # storage='sqlite:///' + os.path.basename(self.path) + '_dummy.db',
1389
1432
  sampler=None, pruner=None, study_name='dummy',
1390
1433
  )
1391
- if len(self.obj_names) == 1:
1434
+ if len(self.all_output_names) == 1:
1392
1435
  kwargs.update(dict(direction='minimize'))
1393
1436
  else:
1394
- kwargs.update(dict(directions=['minimize']*len(self.obj_names)))
1437
+ kwargs.update(dict(directions=['minimize']*len(self.all_output_names)))
1395
1438
  study = optuna.create_study(**kwargs)
1396
1439
 
1397
1440
  # add trial to study
@@ -1441,11 +1484,19 @@ class History:
1441
1484
  )
1442
1485
  trial_kwargs.update(dict(distributions=distributions))
1443
1486
 
1444
- # objective
1445
- if len(self.obj_names) == 1:
1446
- trial_kwargs.update(dict(value=row[self.obj_names].values[0]))
1487
+ # objective (+ constraints + other_outputs as objective)
1488
+ if len(self.all_output_names) == 1:
1489
+ if len(self.obj_names) == 1:
1490
+ trial_kwargs.update(dict(value=row[self.obj_names].values[0]))
1491
+ elif len(self.cns_names) == 1:
1492
+ trial_kwargs.update(dict(value=row[self.cns_names].values[0]))
1493
+ elif len(self.other_output_names) == 1:
1494
+ trial_kwargs.update(dict(value=row[self.other_output_names].values[0]))
1495
+ else:
1496
+ assert False
1447
1497
  else:
1448
- trial_kwargs.update(dict(values=row[self.obj_names].values))
1498
+ values = row[self.all_output_names].values
1499
+ trial_kwargs.update(dict(values=values))
1449
1500
 
1450
1501
  # add to study
1451
1502
  trial = optuna.create_trial(**trial_kwargs)
@@ -99,6 +99,7 @@ class AbstractOptimizer:
99
99
  self.variable_manager = VariableManager()
100
100
  self.objectives = Objectives()
101
101
  self.constraints = Constraints()
102
+ self.other_outputs = Functions()
102
103
 
103
104
  # multi-fidelity
104
105
  self.fidelity = None
@@ -339,6 +340,21 @@ class AbstractOptimizer:
339
340
  _duplicated_name_check(name, self.constraints.keys())
340
341
  self.constraints.update({name: cns})
341
342
 
343
+ def add_other_output(
344
+ self,
345
+ name: str,
346
+ fun: Callable[..., float],
347
+ args: tuple | None = None,
348
+ kwargs: dict | None = None,
349
+ ):
350
+
351
+ other_func = Function()
352
+ other_func.fun = fun
353
+ other_func.args = args or ()
354
+ other_func.kwargs = kwargs or {}
355
+ _duplicated_name_check(name, self.other_outputs.keys())
356
+ self.other_outputs.update({name: other_func})
357
+
342
358
  def add_sub_fidelity_model(
343
359
  self,
344
360
  name: str,
@@ -352,12 +368,12 @@ class AbstractOptimizer:
352
368
  _duplicated_name_check(name, self.sub_fidelity_models.keys())
353
369
  self.sub_fidelity_models._update(name, sub_fidelity_model, fidelity)
354
370
 
355
- def get_variables(self, format='dict'):
371
+ def get_variables(self, format: Literal['dict', 'values', 'raw'] = 'dict'):
356
372
  return self.variable_manager.get_variables(
357
373
  format=format,
358
374
  )
359
375
 
360
- def get_parameter(self, format='dict'):
376
+ def get_parameter(self, format: Literal['dict', 'values', 'raw'] = 'dict'):
361
377
  return self.variable_manager.get_variables(
362
378
  format=format, filter='parameter'
363
379
  )
@@ -399,6 +415,12 @@ class AbstractOptimizer:
399
415
  out.update({name: cns_result})
400
416
  return out
401
417
 
418
+ def _other_outputs(self, out: TrialFunctionOutput) -> TrialFunctionOutput:
419
+ for name, other_func in self.other_outputs.items():
420
+ other_func_result = FunctionResult(other_func, self.fem)
421
+ out.update({name: other_func_result})
422
+ return out
423
+
402
424
  def _get_hard_constraint_violation_names(self, hard_c: TrialConstraintOutput) -> list[str]:
403
425
  violation_names = []
404
426
  for name, result in hard_c.items():
@@ -573,6 +595,7 @@ class AbstractOptimizer:
573
595
 
574
596
  try:
575
597
  y: TrialOutput = opt_._y()
598
+ record.y = y
576
599
  opt_._check_and_raise_interruption()
577
600
 
578
601
  # if intentional error (by user)
@@ -604,7 +627,6 @@ class AbstractOptimizer:
604
627
  _c.update(soft_c)
605
628
  _c.update(hard_c)
606
629
 
607
- record.y = y
608
630
  record.c = _c
609
631
  record.state = TrialState.get_corresponding_state_from_exception(e)
610
632
  record.messages.append(
@@ -619,12 +641,32 @@ class AbstractOptimizer:
619
641
  c.update(soft_c)
620
642
  c.update(hard_c)
621
643
 
644
+ # ===== evaluate other functions =====
645
+ logger.info(_('evaluating other functions...'))
646
+
647
+ other_outputs = TrialFunctionOutput()
648
+ try:
649
+ opt_._other_outputs(other_outputs)
650
+ record.other_outputs = other_outputs
651
+
652
+ # if intentional error (by user)
653
+ except _HiddenConstraintViolation as e:
654
+ _log_hidden_constraint(e)
655
+
656
+ record.other_outputs = other_outputs
657
+ record.state = TrialState.get_corresponding_state_from_exception(e)
658
+ record.messages.append(
659
+ _('Hidden constraint violation during '
660
+ 'another output function evaluation: ')
661
+ + create_err_msg_from_exception(e))
662
+
663
+ raise e
664
+
622
665
  # get values as minimize
623
666
  y_internal: dict = opt_._convert_y(y)
624
667
 
625
668
  logger.info(_('output:'))
626
669
  logger.info(y)
627
- record.y = y
628
670
  record.c = c
629
671
  record.state = TrialState.succeeded
630
672
 
@@ -858,10 +900,11 @@ class AbstractOptimizer:
858
900
  filter='parameter', format='raw'
859
901
  )
860
902
  self.history.finalize(
861
- parameters,
862
- list(self.objectives.keys()),
863
- list(self.constraints.keys()),
864
- [self.sub_fidelity_name] + list(self.sub_fidelity_models.keys()),
903
+ parameters=parameters,
904
+ obj_names=list(self.objectives.keys()),
905
+ cns_names=list(self.constraints.keys()),
906
+ other_output_names=list(self.other_outputs.keys()),
907
+ sub_fidelity_names=[self.sub_fidelity_name] + list(self.sub_fidelity_models.keys()),
865
908
  additional_data=self._collect_additional_data()
866
909
  )
867
910
 
@@ -28,7 +28,9 @@ __all__ = [
28
28
  'TrialInput',
29
29
  'TrialOutput',
30
30
  'TrialConstraintOutput',
31
+ 'TrialFunctionOutput',
31
32
  'Function',
33
+ 'FunctionResult',
32
34
  'Functions',
33
35
  'Objective',
34
36
  'ObjectiveResult',
@@ -138,6 +140,12 @@ class Objective(Function):
138
140
  return self._convert(value, self.direction)
139
141
 
140
142
 
143
+ class FunctionResult:
144
+
145
+ def __init__(self, func: Function, fem: AbstractFEMInterface):
146
+ self.value: float = func.eval(fem)
147
+
148
+
141
149
  class ObjectiveResult:
142
150
 
143
151
  def __init__(self, obj: Objective, fem: AbstractFEMInterface, obj_value: float = None):
@@ -302,3 +310,4 @@ SubSampling: TypeAlias = int
302
310
  TrialInput: TypeAlias = dict[str, Variable]
303
311
  TrialOutput: TypeAlias = dict[str, ObjectiveResult]
304
312
  TrialConstraintOutput: TypeAlias = dict[str, ConstraintResult]
313
+ TrialFunctionOutput: TypeAlias = dict[str, FunctionResult]
@@ -304,7 +304,7 @@ class VariableManager:
304
304
  filter: (Literal['pass_to_fem', 'parameter']
305
305
  | tuple[Literal['pass_to_fem', 'parameter']]
306
306
  | None) = None, # 'pass_to_fem' and 'parameter' (OR filter)
307
- format: str = None, # None, 'dict' and 'values'
307
+ format: Literal['dict', 'values', 'raw'] | None = None, # Defaults to 'raw'
308
308
  ) -> (
309
309
  dict[str, Variable]
310
310
  | dict[str, Parameter]